├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── keyss │ │ └── view_record_demo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── keyss │ │ │ └── view_record_demo │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── io │ └── keyss │ └── view_record_demo │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib-view-record ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── keyss │ │ └── view_record │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── keyss │ │ └── view_record │ │ ├── ISourceProvider.kt │ │ ├── RecordAsyncEncoder.kt │ │ ├── RecordEncoder.kt │ │ ├── VideoRecordConfig.kt │ │ ├── audio │ │ ├── AudioEncoder.java │ │ ├── AudioPostProcessEffect.kt │ │ ├── CustomAudioEffect.kt │ │ ├── GetAacData.kt │ │ ├── GetMicrophoneData.kt │ │ ├── MicrophoneManager.java │ │ └── NoAudioEffect.kt │ │ ├── base │ │ ├── BaseEncoder.java │ │ └── Frame.kt │ │ ├── recording │ │ ├── AndroidMuxerRecordController.java │ │ ├── BaseRecordController.java │ │ ├── RecordController.java │ │ └── ViewRecorder.kt │ │ ├── utils │ │ ├── CodecUtil.java │ │ ├── ColorFormatUtil.kt │ │ ├── EncoderTools.kt │ │ ├── FrameUtil.kt │ │ ├── RecordViewUtil.kt │ │ ├── VRLogger.kt │ │ └── yuv │ │ │ └── ConvertUtil.java │ │ └── video │ │ ├── EncoderCallback.kt │ │ ├── EncoderErrorCallback.kt │ │ ├── FormatVideoEncoder.java │ │ ├── FpsLimiter.java │ │ ├── GetVideoData.kt │ │ ├── IFrameDataGetter.kt │ │ └── VideoEncoder.java │ └── test │ └── java │ └── io │ └── keyss │ └── view_record │ └── ExampleUnitTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/caches 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | /.idea/navEditor.xml 8 | /.idea/assetWizardSettings.xml 9 | /build 10 | /captures 11 | .cxx 12 | 13 | 14 | # Built application files 15 | *.apk 16 | *.ap_ 17 | 18 | # Files for the ART/Dalvik VM 19 | *.dex 20 | 21 | # Java class files 22 | *.class 23 | 24 | # Generated files 25 | bin/ 26 | gen/ 27 | out/ 28 | 29 | # Gradle files 30 | .gradle/ 31 | build/ 32 | 33 | # Local configuration file (sdk path, etc) 34 | local.properties 35 | 36 | # Proguard folder generated by Eclipse 37 | proguard/ 38 | 39 | # Log Files 40 | *.log 41 | 42 | # Android Studio Navigation editor temp files 43 | .navigation/ 44 | 45 | # Android Studio captures folder 46 | captures/ 47 | 48 | # Intellij 49 | *.iml 50 | *.ipr 51 | *.iws 52 | .idea/ 53 | 54 | # Keystore files 55 | #*.jks 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | 60 | # Google Services (e.g. APIs or Firebase) 61 | google-services.json 62 | 63 | # Freeline 64 | freeline.py 65 | freeline/ 66 | freeline_project_description.json 67 | 68 | # fastlane 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | fastlane/readme.md 74 | 75 | # Eclipse project files 76 | .classpath 77 | .projec 78 | 79 | # Windows clutter 80 | Thumbs.db 81 | # Mac OS 82 | .DS_Store 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Key 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android 应用内录屏 2 | 3 | ## Description 4 | Android View Record: Screen Record, Video Record, Audio Record 5 | 安卓录屏,或者说app内录屏,避开隐私问题,不会录到系统通知或者其他系统级弹窗或者各种类似下拉状态栏等会遮盖的之类的画面,当初也是为了实现这个需求造的这个轮子(苦于搜遍GitHub都没搜到,不然也懒得写了),View级别的录屏,你可以录整个RootContentView,也可以是只录某个ViewGroup或者View,支持同时录音 6 | 7 | > 注释保留了很多,因为本身也是边学习边输出,出于交流分享共同学习的目的,保留了注释,方便大家理解和避开一些坑 8 | 9 | ## 2024/05/20 10 | NOTE: 已知在就算支持的颜色格式中,也可能会出现oom,如420Planar在荣耀某款平板上,华为(确切的说是海思或者麒麟的芯片)在颜色格式上,support列表中是支持但是还是有很多不支持的问题,会导致OOM,网站这个问题遇到的也很多,一搜一大把,大致Log类似于: 11 | ```logcatfilter 12 | W hw configLocalPlayBack err = -22 13 | W do not know color format 0x7f000001 = 2130706433 14 | I setupAVCEncoderParameters with [profile: Baseline] [level: Level3] 15 | I [OMX.hisi.video.encoder.avc] cannot encode color aspects. Ignoring. 16 | I [OMX.hisi.video.encoder.avc] cannot encode HDR static metadata. Ignoring. 17 | I setupVideoEncoder succeeded 18 | I [OMX.hisi.video.encoder.avc] got color aspects (R:0(Unspecified), P:0(Unspecified), M:0(Unspecified), T:0(Unspecified)) err=-1010(??) 19 | ``` 20 | ```logcatfilter 21 | # 又发现一列,又是19,420Planar,暂时先把19放到最后选择吧 22 | # 但是在荣耀9Lite上是正常的,编码器同为OMX.IMG.TOPAZ.VIDEO.Encoder,但荣耀9Lite支持列表颜色更多 23 | # 荣耀畅玩 7X/BND-AL10 Log 24 | hw configLocalPlayBack err = -1010 25 | do not know color format 0x7f000001 = 2130706433 26 | setupAVCEncoderParameters with [profile: Baseline] [level: Level52] 27 | [OMX.IMG.TOPAZ.VIDEO.Encoder] cannot encode color aspects. Ignoring. 28 | [OMX.IMG.TOPAZ.VIDEO.Encoder] cannot encode HDR static metadata. Ignoring. 29 | setupVideoEncoder succeeded 30 | E signalError(omxError 0x80001001, internalError -12) 31 | E Codec reported err 0xfffffff4, actionCode 0, while in state 5 32 | E [OMX.IMG.TOPAZ.VIDEO.Encoder] ERROR(0x80001000) 33 | E signalError(omxError 0x80001000, internalError -2147483648) 34 | E Codec reported err 0x80001000, actionCode 0, while in state 0 35 | ``` 36 | 展示解决方案只能是不使用19(COLOR_FormatYUV420Planar),21(COLOR_FormatYUV420SemiPlanar)没问题,所以我新增了一个优选列表,优先选择21 37 | 并且OMX.google.h264.encoder这个编码器在华为上也无法使用,编码的视频有问题无法播放,别的机型测了几款没问题。 38 | 39 | ## 2024/04/09 修复在Android14上的采集问题 40 | 主要是在PixelCopy.request中,不像前代api会阻塞返回了,正如之前在RecordViewUtil中134行注释中所说的,在这一代api中突然生效了 41 | 42 | 目前已知在Android7.1.1 API25上还有问题,Muxer相关 43 | 44 | 并且在非采用PixelCopy.request方案的录制中,采集摄像头View需使用TextureView,直接使用CameraX中的Texture无法采集到,会是黑屏 45 | 46 | ## Nov 23 2023 新增一个录制类 ViewRecorder 47 | **高可用,比我之前写的那个兼容性要好,暂时项目中先用这个** 48 | 不过因为也是临时用,又紧急,所以一些可配置下项我暂时还没抽出来,只根据自己项目需要写死了值 49 | 50 | 借用了pedroSG94在他的RootEncoder推流项目中的录制编码部分类进行修改 51 | 感谢大佬的开源分享,项目地址:https://github.com/pedroSG94/RootEncoder 52 | 原版本在机型兼容性上存在一些问题,由于比较紧急,暂时没时间调试,所以先在该项目基础上写一版能用的,这个项目考虑的还是比较完整的。 53 | 54 | 他的代码已经写的很好了,不过很复杂不便于学习,我足足看了两天,interface很绕,我删除了部分他推流用的功能,做了些许精简 55 | 他的项目是流,所以使用的是ArrayBlockingQueue,而我是实时采集,所以Video这部分我加了判断但还没有删除,Audio部分则是删除了。 56 | 后面有时间继续学习他的编码部分,完善下我这个简单的单纯的录屏功能 57 | 中间还参考了两个项目,一个微软的,一个chromium的 58 | https://github.com/microsoft/HydraLab/blob/b50bc6054d32a1b83d6c944ef9fbd393150922ab/android_client/app/src/main/java/com/microsoft/hydralab/android/client/BaseEncoder.java 59 | https://github.com/chromium/chromium/blob/0ddb38eda131f19995ec537bc67b62d35170e2ab/media/base/android/java/src/org/chromium/media/MediaCodecUtil.java 60 | 能参考的项目基本上都是推流中使用的。 61 | chromium中对硬编部分,做了CPU判断,从我的代码上线至今收集到的日志,应该是有点关系的。后续我也会跟进完善 62 | 63 | 64 | ## Usage 65 | 具体可以参照Demo,MainActivity中的调用 66 | 67 | * 最新的方案参考method4(view: View)方法 68 | 69 | **旧:** 70 | 1. 实现```ISourceProvider```接口 71 | 2. 指定输出路径 72 | 3. 设定参数 73 | 1. 可配置项很多,可以看类中没有private的成员变量 74 | ```kotlin 75 | // 比如视频的格式类型 acv h264, hevc h265,但要注意,很多手机还不能硬解硬编h265 76 | videoMimeType 77 | ``` 78 | 4. 开始录屏 79 | ```kotlin 80 | val outputFile = File(externalCacheDir, "record_${System.currentTimeMillis()}.mp4") 81 | val sourceProvider = object : ISourceProvider { 82 | override fun next(): Bitmap { 83 | return RecordViewUtil.getBitmapFromView(window, view, 800) 84 | } 85 | 86 | override fun onResult(isSuccessful: Boolean, result: String) { 87 | Log.w(TAG, "onResult() isSuccessful: $isSuccessful, result: $result") 88 | } 89 | } 90 | // 方案1:便捷方式 91 | mRecordEncoder.start(sourceProvider, outputFile, 1024_000, true) 92 | // 方案2: 先设定好参数,在业务处启动 93 | mRecordEncoder.setUp(sourceProvider, outputFile, 1024_000, true) 94 | mRecordEncoder.start() 95 | // 方案3: 无需读写权限,等结束回调返回路径 96 | mRecordEncoder.start( 97 | window, 98 | view, 99 | isRecordAudio = false 100 | ) { isSuccessful: Boolean, result: String -> 101 | Log.w(TAG, "onResult() isSuccessful: $isSuccessful, result: $result") 102 | } 103 | ``` 104 | 5. 要排查问题可以打开日志输出 105 | ```kotlin 106 | VRLogger.logLevel = Log.VERBOSE 或者 Log.DEBUG 级别 107 | ``` 108 | 109 | 110 | ## 优点: 111 | 1. 不需要权限,主要说得是录屏的那个敏感权限弹窗。如果需要录音,那录音权限还是需要的。 112 | 2. 解决隐私问题,常规录屏方案可能会录到用户的隐私信息,比如聊天记录、各类账号、短信验证码等。 113 | 3. 不会因为下拉状态栏等,需要录制的内容被遮盖。 114 | 4. 自定义源更灵活。 115 | 116 | ## 缺点: 117 | 1. 只能录制应用内的前台内容(录后台内容其实可以实现,自己控制下next的输入源就可以了,理论上扩展性很高)。 118 | 2. 不能录制SurfaceView的内容,原理问题,如果需要录摄像头或者Player需要使用TextureView。 119 | 120 | 121 | ## 难点 122 | 1. 帧率控制。(已解决) 123 | 2. 启动帧同步,目前采用视频帧启动后再输入音频。目前还是会丢启动时的部分帧,待解决 124 | 3. 不同机型的颜色格式适配,暂未完全解决。(大致解决,具体原因看5/20那条更新) 125 | 4. 视频输入源耗时问题,会导致实际帧率下降,大分辨率的情况下比较明显,如果异步采集合成,时间一长,积压的帧多了内存会爆炸,后续可能需要改算法方案来提高效率。 126 | 127 | 128 | ## To-Do List 129 | - [ ] 代码优化抽象,方便后续升级扩展(第一个版本写的比较急) 130 | - [ ] 上传到公共仓库 131 | - [x] Bitmap - Pixels 算法效率提升 132 | - [x] 机型适配 133 | - [x] 帧率提升 134 | - [x] 麦克风降噪 135 | - [ ] 合入其他音频源 136 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.keyss.view_record_demo' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "io.keyss.view_record_demo" 12 | minSdk 23 13 | targetSdk 33 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation(project(':lib-view-record')) 37 | //implementation 'io.keyss.android.library:view-record:1.0.8' 38 | 39 | 40 | 41 | implementation 'androidx.core:core-ktx:1.10.1' 42 | implementation 'androidx.appcompat:appcompat:1.6.1' 43 | implementation 'com.google.android.material:material:1.9.0' 44 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 45 | testImplementation 'junit:junit:4.13.2' 46 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 48 | 49 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 50 | 51 | // CameraX core library using the camera2 implementation 52 | def camerax_version = "1.3.0-beta01" 53 | // The following line is optional, as the core library is included indirectly by camera-camera2 54 | //implementation "androidx.camera:camera-core:${camerax_version}" 55 | implementation "androidx.camera:camera-camera2:${camerax_version}" 56 | // If you want to additionally use the CameraX Lifecycle library 57 | implementation "androidx.camera:camera-lifecycle:${camerax_version}" 58 | // If you want to additionally use the CameraX VideoCapture library 59 | implementation "androidx.camera:camera-video:${camerax_version}" 60 | // If you want to additionally use the CameraX View class 61 | implementation "androidx.camera:camera-view:${camerax_version}" 62 | // If you want to additionally add CameraX ML Kit Vision Integration 63 | //implementation "androidx.camera:camera-mlkit-vision:${camerax_version}" 64 | // If you want to additionally use the CameraX Extensions library 65 | //implementation "androidx.camera:camera-extensions:${camerax_version}" 66 | // 完整版 67 | //implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer:v8.4.0-release-jitpack' 68 | //implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' 69 | //implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' 70 | // exo 71 | //implementation 'com.google.android.exoplayer:exoplayer:2.19.0' 72 | } 73 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/io/keyss/view_record_demo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.keyss.view_record_demo 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.keyss.view_record_demo", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/keyss/view_record_demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.keyss.view_record_demo 2 | 3 | import android.graphics.Bitmap 4 | import android.media.MediaCodec 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | import android.widget.Toast 9 | import android.widget.VideoView 10 | import androidx.activity.result.ActivityResultLauncher 11 | import androidx.activity.result.contract.ActivityResultContracts 12 | import androidx.appcompat.app.AlertDialog 13 | import androidx.appcompat.app.AppCompatActivity 14 | import androidx.appcompat.widget.AppCompatImageView 15 | import androidx.camera.core.Camera 16 | import androidx.camera.core.CameraSelector 17 | import androidx.camera.core.Preview 18 | import androidx.camera.lifecycle.ProcessCameraProvider 19 | import androidx.camera.view.PreviewView 20 | import androidx.constraintlayout.utils.widget.MotionButton 21 | import androidx.core.content.ContextCompat 22 | import androidx.core.view.isGone 23 | import androidx.core.view.isVisible 24 | import androidx.lifecycle.LifecycleOwner 25 | import androidx.lifecycle.lifecycleScope 26 | import com.google.common.util.concurrent.ListenableFuture 27 | import io.keyss.view_record.ISourceProvider 28 | import io.keyss.view_record.RecordEncoder 29 | import io.keyss.view_record.recording.RecordController 30 | import io.keyss.view_record.recording.ViewRecorder 31 | import io.keyss.view_record.utils.CodecUtil 32 | import io.keyss.view_record.utils.RecordViewUtil 33 | import io.keyss.view_record.utils.VRLogger 34 | import io.keyss.view_record.video.EncoderErrorCallback 35 | import kotlinx.coroutines.Job 36 | import kotlinx.coroutines.isActive 37 | import kotlinx.coroutines.launch 38 | import java.io.File 39 | import java.util.Arrays 40 | 41 | class MainActivity : AppCompatActivity() { 42 | private val TAG = "MainTAG" 43 | private lateinit var previewView: PreviewView 44 | private lateinit var layoutRecordContentView: androidx.constraintlayout.widget.ConstraintLayout 45 | private lateinit var tvTime: androidx.appcompat.widget.AppCompatTextView 46 | private lateinit var btnStart: com.google.android.material.button.MaterialButton 47 | private lateinit var btnStop: MotionButton 48 | private lateinit var btnCapture: MotionButton 49 | private lateinit var ivCapture: AppCompatImageView 50 | private lateinit var videoView: VideoView 51 | 52 | private var mCamera: Camera? = null 53 | private lateinit var cameraProviderFuture: ListenableFuture 54 | 55 | private var mTimerJob: Job? = null 56 | 57 | //private val mRecordEncoder = RecordAsyncEncoder() 58 | private val mRecordEncoder = RecordEncoder() 59 | 60 | private val viewRecord = ViewRecorder() 61 | 62 | private var mLastRecordFile: File? = null 63 | 64 | // 需要的权限列表 65 | private val permissionMap = mutableMapOf( 66 | android.Manifest.permission.CAMERA to false, 67 | android.Manifest.permission.RECORD_AUDIO to false, 68 | ) 69 | private lateinit var requestPermissionLauncher: ActivityResultLauncher 70 | 71 | override fun onCreate(savedInstanceState: Bundle?) { 72 | super.onCreate(savedInstanceState) 73 | setContentView(R.layout.activity_main) 74 | previewView = findViewById(R.id.preview_view_main_activity) 75 | layoutRecordContentView = findViewById(R.id.layout_record_content_main_activity) 76 | tvTime = findViewById(R.id.tv_time_main_activity) 77 | btnStart = findViewById(R.id.btn_start_record_main_activity) 78 | btnStart.setOnClickListener { 79 | startRecord(layoutRecordContentView) 80 | } 81 | btnStop = findViewById(R.id.btn_stop_record_main_activity) 82 | btnStop.setOnClickListener { 83 | stopRecord() 84 | } 85 | videoView = findViewById(R.id.video_view_main_activity) 86 | videoView.setOnCompletionListener { 87 | videoView.isGone = true 88 | Toast.makeText(this, "播放结束", Toast.LENGTH_SHORT).show() 89 | } 90 | findViewById(R.id.btn_play_main_activity).setOnClickListener { 91 | if (mLastRecordFile?.absolutePath.isNullOrBlank()) { 92 | Toast.makeText(this, "请先录制", Toast.LENGTH_SHORT).show() 93 | return@setOnClickListener 94 | } 95 | videoView.isVisible = true 96 | videoView.setVideoPath(mLastRecordFile?.absolutePath) 97 | videoView.start() 98 | } 99 | 100 | ivCapture = findViewById(R.id.iv_capture_main_activity) 101 | btnCapture = findViewById(R.id.btn_capture_record_main_activity) 102 | btnCapture.setOnClickListener { 103 | val startTime = System.currentTimeMillis() 104 | val frameBitmap = RecordViewUtil.getBitmapFromView(window, layoutRecordContentView, 540) 105 | //val frameBitmap = viewRecord.getFrameBitmap(640) 106 | Log.i(TAG, "OnClick getFrameBitmap cost: ${System.currentTimeMillis() - startTime}ms") 107 | ivCapture.setImageBitmap(frameBitmap) 108 | } 109 | initFunc() 110 | VRLogger.logLevel = Log.VERBOSE 111 | checkPermission() 112 | 113 | test2() 114 | } 115 | 116 | private fun test2() { 117 | val mime = "video/avc" 118 | //CodecUtil.getAllHardwareEncoders("video/avc") 119 | val allHardwareEncoders = CodecUtil.getAllHardwareEncoders(mime) 120 | for (mediaCodecInfo in allHardwareEncoders) { 121 | Log.i(TAG, "HardwareEncoders: " + mediaCodecInfo.name) 122 | Log.i( 123 | TAG, 124 | "Color supported by this encoder: " + Arrays.toString(mediaCodecInfo.getCapabilitiesForType(mime).colorFormats) 125 | ) 126 | } 127 | val allSoftwareEncoders = CodecUtil.getAllSoftwareEncoders(mime) 128 | for (mediaCodecInfo in allSoftwareEncoders) { 129 | Log.i(TAG, "SoftwareEncoders: " + mediaCodecInfo.name) 130 | Log.i( 131 | TAG, 132 | "Color supported by this encoder: " + Arrays.toString(mediaCodecInfo.getCapabilitiesForType(mime).colorFormats) 133 | ) 134 | } 135 | } 136 | 137 | private fun initFunc() { 138 | requestPermissionLauncher = 139 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> 140 | // 141 | Log.d(TAG, "requestPermissionLauncher request Permission result isGranted=$isGranted") 142 | /*if (isGranted) { 143 | // Permission is granted. Continue the action or workflow in your app. 144 | startPreview() 145 | } else { 146 | // Explain to the user that the feature is unavailable because the 147 | // feature requires a permission that the user has denied. At the 148 | // same time, respect the user's decision. Don't link to system 149 | // settings in an effort to convince the user to change their 150 | // decision. 151 | // 向用户解释该功能不可用,因为该功能需要用户拒绝的权限。 同时尊重用户的决定。 不要链接到系统设置以说服用户改变他们的决定。 152 | checkPermission() 153 | }*/ 154 | // 需要再次检测,同时满足两个权限 155 | checkPermission() 156 | } 157 | 158 | // todo 补充一个一次请求多个权限的方法 159 | /*val requestMultiplePermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){ 160 | 161 | } 162 | requestMultiplePermissionLauncher.launch(permissionMap.mapTo(arrayOf(), { 163 | 164 | }))*/ 165 | } 166 | 167 | 168 | private var mStartTime = 0L 169 | private fun startRecord(view: View) { 170 | if (isLackPermissions()) { 171 | checkPermission() 172 | return 173 | } 174 | tvTime.text = "开始" 175 | //method1(view) 176 | //method2(view) 177 | //method3(view) 178 | method4(view) 179 | //mRecordEncoder.setUp(sourceProvider, outputFile, 1024_000, true) 180 | //mRecordEncoder.start() 181 | mTimerJob = lifecycleScope.launch { 182 | var time = 0 183 | while (this.isActive) { 184 | tvTime.text = "${time++}" 185 | kotlinx.coroutines.delay(1000) 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * 测试手动初始化 192 | */ 193 | private fun init(view: View) { 194 | viewRecord.init( 195 | window = window, 196 | view = view, 197 | width = 555, 198 | fps = 30, 199 | videoBitRate = 4_000_000, 200 | iFrameInterval = 1, 201 | audioBitRate = 192_000, 202 | audioSampleRate = 44100, 203 | isStereo = true, 204 | ) 205 | /*viewRecord.initJustVideo( 206 | window = window, 207 | view = view, 208 | width = 222, 209 | fps = 30, 210 | videoBitRate = 4_000_000, 211 | iFrameInterval = 1, 212 | )*/ 213 | } 214 | 215 | private fun method4(view: View) { 216 | if (viewRecord.isStartRecord) { 217 | Toast.makeText(this, "正在录制中", Toast.LENGTH_SHORT).show() 218 | return 219 | } 220 | init(view) 221 | val outputFile = File(externalCacheDir, "record_${System.currentTimeMillis()}.mp4") 222 | mLastRecordFile = outputFile 223 | // 先check一下,最后对比下参数 224 | val width = view.width 225 | val height = view.height 226 | Log.i(TAG, "startRecord(): View: width=$width, height=$height, outputFile: ${outputFile.absolutePath}") 227 | mStartTime = System.currentTimeMillis() 228 | viewRecord.startRecord(outputFile.absolutePath, object : RecordController.Listener { 229 | override fun onStatusChange(status: RecordController.Status?) { 230 | Log.i(TAG, "onStatusChange() called with: status = $status") 231 | } 232 | }, object : EncoderErrorCallback { 233 | override fun onCodecError(type: String, e: MediaCodec.CodecException) { 234 | Log.e(TAG, "onCodecError() called with: type = $type", e) 235 | } 236 | }) 237 | } 238 | 239 | private fun stopMethod4(): Unit { 240 | viewRecord.stopRecord() 241 | } 242 | 243 | private fun stopMethod3(): Unit { 244 | } 245 | 246 | private fun method3(view: View) { 247 | 248 | } 249 | 250 | private fun method2(view: View) { 251 | mRecordEncoder.start( 252 | window, 253 | view, 254 | isRecordAudio = false 255 | ) { isSuccessful: Boolean, result: String -> 256 | Log.w(TAG, "onResult() isSuccessful: $isSuccessful, result: $result") 257 | } 258 | } 259 | 260 | private fun method1(view: View) { 261 | val outputFile = File(externalCacheDir, "record_${System.currentTimeMillis()}.mp4") 262 | mLastRecordFile = outputFile 263 | Log.i(TAG, "startRecord() outputFile: ${outputFile.absolutePath}") 264 | val sourceProvider = object : ISourceProvider { 265 | override fun next(): Bitmap { 266 | return RecordViewUtil.getBitmapFromView(window, view, 540) 267 | } 268 | 269 | override fun onResult(isSuccessful: Boolean, result: String) { 270 | Log.w(TAG, "onResult() isSuccessful: $isSuccessful, result: $result") 271 | } 272 | } 273 | mRecordEncoder.start(sourceProvider, outputFile, 1024_000, true) 274 | } 275 | 276 | private fun stopRecord() { 277 | // 录制时长 278 | val duration = System.currentTimeMillis() - mStartTime 279 | // 输出转换成秒毫秒 280 | val durationStr = "${duration / 1000}.${duration % 1000}" 281 | //mRecordEncoder.stop() 282 | stopMethod4() 283 | mTimerJob?.cancel() 284 | mTimerJob = null 285 | Log.i(TAG, "stopRecord() duration=${durationStr}秒, RecordFile: ${mLastRecordFile?.absolutePath}") 286 | } 287 | 288 | private fun checkPermission() { 289 | permissionMap.keys.forEach { 290 | permissionMap[it] = 291 | ContextCompat.checkSelfPermission(this, it) == android.content.pm.PackageManager.PERMISSION_GRANTED 292 | } 293 | Log.i(TAG, "checkPermission(): $permissionMap") 294 | // 全部都是true有权限 295 | if (!isLackPermissions()) { 296 | startPreview() 297 | return 298 | } 299 | permissionMap.forEach { 300 | if (!it.value) { 301 | tryRequestOnePermission(it.key) 302 | // 只能一个一个来,两个一起来,另一个直接返回isGranted: false 303 | return 304 | } 305 | } 306 | } 307 | 308 | /** 309 | * 是否缺少权限 310 | */ 311 | private fun isLackPermissions() = permissionMap.containsValue(false) 312 | 313 | /** 314 | * @param isExplained 是否已经解释过 315 | */ 316 | private fun tryRequestOnePermission(permission: String, isExplained: Boolean = false) { 317 | val shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale(permission) 318 | Log.d( 319 | TAG, 320 | "请求单个权限(${permission}) 需要展示请求权限的理由吗?=$shouldShowRequestPermissionRationale,是否已展示过理由=$isExplained" 321 | ) 322 | if (shouldShowRequestPermissionRationale) { 323 | // 向用户显示指导界面,在此界面中说明用户希望启用的功能为何需要特定权限。 324 | if (isExplained) { 325 | requestOnePermission(permission) 326 | } else { 327 | showWhy(permission) 328 | } 329 | } else { 330 | requestOnePermission(permission) 331 | } 332 | } 333 | 334 | private fun requestOnePermission(permission: String) { 335 | requestPermissionLauncher.launch(permission) 336 | } 337 | 338 | private fun showWhy(permission: String) { 339 | val message = when (permission) { 340 | android.Manifest.permission.CAMERA -> "需要相机权限,用于录像" 341 | android.Manifest.permission.RECORD_AUDIO -> "需要录音权限,用于录音" 342 | else -> "需要权限" 343 | } 344 | AlertDialog.Builder(this) 345 | .setTitle("提示") 346 | .setMessage(message) 347 | .setPositiveButton("确定") { dialog, which -> 348 | Log.i(TAG, "showWhy Dialog: current state = ${lifecycle.currentState}") 349 | tryRequestOnePermission(permission, true) 350 | dialog.dismiss() 351 | } 352 | .setNegativeButton("取消") { dialog, which -> 353 | // TODO: 2023/5/26 你的操作 354 | dialog.dismiss() 355 | } 356 | .show() 357 | } 358 | 359 | private fun startPreview() { 360 | cameraProviderFuture = ProcessCameraProvider.getInstance(this) 361 | cameraProviderFuture.addListener({ 362 | val cameraProvider = cameraProviderFuture.get() 363 | bindPreview(cameraProvider) 364 | }, ContextCompat.getMainExecutor(this)) 365 | } 366 | 367 | private fun bindPreview(cameraProvider: ProcessCameraProvider) { 368 | val preview: Preview = Preview.Builder().build() 369 | 370 | val cameraSelector: CameraSelector = CameraSelector.Builder() 371 | .requireLensFacing(CameraSelector.LENS_FACING_FRONT) 372 | .build() 373 | 374 | preview.setSurfaceProvider(previewView.surfaceProvider) 375 | 376 | mCamera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, preview) 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 28 | 29 | 37 | 38 | 46 | 47 | 54 | 55 | 63 | 64 | 71 | 72 | 83 | 84 | 90 | 91 | 97 | 98 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ViewRecord 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |