(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 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/test/java/io/keyss/view_record_demo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record_demo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | println("1 or 1=${1 or 1}")
16 | println("1 or 0=${1 or 0}")
17 | println("0 or 0=${0 or 0}")
18 | println("1 and 0=${1 and 0}")
19 | println("1 and 1=${1 and 1}")
20 | println("0 and 0=${0 and 0}")
21 |
22 | println()
23 | println()
24 |
25 | println("-1 or -1=${-1 or -1}")
26 | println("-1 or 0=${-1 or 0}")
27 | println("0 or 0=${0 or 0}")
28 | println("-1 and 0=${-1 and 0}")
29 | println("-1 and -1=${-1 and -1}")
30 | println("0 and 0=${0 and 0}")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '7.4.2' apply false
4 | id 'com.android.library' version '7.4.2' apply false
5 | id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
6 | }
7 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jun 06 18:01:26 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/lib-view-record/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/lib-view-record/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | //ext.libVersion = "1.0.2-kt1.6.10"
7 | ext.libVersion = "1.0.11"
8 |
9 | android {
10 | namespace 'io.keyss.view_record'
11 | compileSdk 33
12 |
13 | defaultConfig {
14 | minSdk 23
15 | targetSdk 33
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | consumerProguardFiles "consumer-rules.pro"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_17
29 | targetCompatibility JavaVersion.VERSION_17
30 | }
31 | kotlinOptions {
32 | jvmTarget = '17'
33 | }
34 | }
35 |
36 | dependencies {
37 | compileOnly 'androidx.annotation:annotation:1.7.0'
38 | testImplementation 'junit:junit:4.13.2'
39 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
41 | }
42 |
43 | // 将library上传到mavenCenter的脚本
44 | apply from: "../../../public/zxslLibraryMavenUploader.gradle"
45 |
--------------------------------------------------------------------------------
/lib-view-record/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Key-CN/ViewRecord/2ca4039148584df84e3ca163f77c082de997f3a5/lib-view-record/consumer-rules.pro
--------------------------------------------------------------------------------
/lib-view-record/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
--------------------------------------------------------------------------------
/lib-view-record/src/androidTest/java/io/keyss/view_record/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("io.keyss.view_record.test", appContext.packageName)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/ISourceProvider.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record
2 |
3 | import android.graphics.Bitmap
4 |
5 | /**
6 | * @author Key
7 | * Time: 2022/10/11 16:27
8 | * Description:
9 | */
10 | interface ISourceProvider {
11 | /**
12 | * 下一帧图像
13 | */
14 | operator fun next(): Bitmap
15 |
16 | /**
17 | * 结果回调
18 | */
19 | fun onResult(isSuccessful: Boolean, result: String)
20 | }
21 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/RecordAsyncEncoder.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record
2 |
3 | import android.graphics.Bitmap
4 | import android.media.MediaCodec
5 | import android.media.MediaFormat
6 | import android.media.MediaMuxer
7 | import android.view.View
8 | import android.view.Window
9 | import androidx.annotation.RequiresPermission
10 | import io.keyss.view_record.utils.EncoderTools
11 | import io.keyss.view_record.utils.RecordViewUtil
12 | import io.keyss.view_record.utils.VRLogger
13 | import java.io.File
14 | import java.nio.ByteBuffer
15 |
16 |
17 | /**
18 | * @author Key
19 | * Time: 2023/11/20 20:40
20 | * Description: 录制及编码
21 | * 改良,未完成
22 | */
23 | class RecordAsyncEncoder {
24 | // 从我打印的capabilitiesForType.colorFormats来看,确实全部都支持:2135033992,另外出现的较多的是COLOR_FormatSurface
25 | // 从测试结果看 全部采用COLOR_FormatYUV420Flexible在某些机型上会导致花屏
26 | //private var mColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
27 | private var mColorFormat = 0
28 |
29 | @Volatile
30 | private var isVideoStarted = false
31 |
32 | /**
33 | * running是运行完,因为stop之后最后一帧还需要时间来保存
34 | */
35 | @Volatile
36 | private var isRunning = false
37 |
38 | /**
39 | * 只回调一次
40 | */
41 | @Volatile
42 | private var isResulted = false
43 |
44 | private lateinit var mMediaMuxer: MediaMuxer
45 |
46 | @Volatile
47 | private var isMuxerStarted = false
48 | private var mRecordStartTime = -1L
49 | private var mLastFrameTime = -1L
50 |
51 | ////// Video
52 | private lateinit var mVideoRecordConfig: VideoRecordConfig
53 |
54 | // acv h264, hevc h265, 根据需要求改
55 | var videoMimeType = MediaFormat.MIMETYPE_VIDEO_AVC
56 |
57 | /**
58 | * 默认帧率(最大帧率)采用电视级的24帧每秒,大部分fps都采用的不是整数
59 | * 为了让参数利于计算,且缩小文件尺寸,改为20
60 | * 实际视频是动态帧率
61 | */
62 | var frameRate: Float = 20f
63 | set(value) {
64 | field = value
65 | fpsMs = 1000.0 / value
66 | }
67 |
68 | /** 每帧时间,仅为方便计算使用 */
69 | var fpsMs: Double = 1000.0 / frameRate
70 | private set
71 |
72 | /**
73 | * I帧间隔:秒
74 | */
75 | var iFrameInterval = 1f
76 |
77 | /** 视频比特率 */
78 | private var mVideoBitRate = 512_000
79 |
80 | /**
81 | * 数据源
82 | */
83 | private lateinit var mSourceProvider: ISourceProvider
84 |
85 | /**
86 | * 输出文件,存在则覆盖
87 | */
88 | private lateinit var mOutputFile: File
89 |
90 | /** 输入流Buffer超时,微秒 */
91 | var defaultTimeOutUs: Long = 0
92 |
93 | /**
94 | * 前置配置,可以不从start加参数
95 | */
96 | fun setUp(provider: ISourceProvider, outputFile: File, minBitRate: Int, isRecordAudio: Boolean = true) {
97 | if (isVideoStarted || isRunning) {
98 | return
99 | }
100 | mOutputFile = outputFile
101 | mSourceProvider = provider
102 | mVideoBitRate = minBitRate
103 | prepare()
104 | }
105 |
106 | private fun prepare() {
107 | val bitmap = try {
108 | mSourceProvider.next()
109 | } catch (e: Exception) {
110 | VRLogger.e("初始化错误: 第一次取bitmap异常", e)
111 | onError("初始化错误:无法获取到屏幕图像或者超过内存大小")
112 | return
113 | }
114 | try {
115 | init(bitmap.width, bitmap.height, mVideoBitRate)
116 | } catch (e: Exception) {
117 | onError("初始化错误:${e.message}")
118 | return
119 | }
120 | }
121 |
122 | @Synchronized
123 | fun start() {
124 | if (isVideoStarted || isRunning) {
125 | return
126 | }
127 | if (::mSourceProvider.isInitialized.not() || ::mOutputFile.isInitialized.not()) {
128 | onError("请先调用setUp()方法")
129 | return
130 | }
131 | isResulted = false
132 | isVideoStarted = false
133 | isMuxerStarted = false
134 | ////////////////////////////////////////////////
135 | isRunning = true
136 | runVideo()
137 | }
138 |
139 | /**
140 | * 自定义源更灵活
141 | */
142 | fun start(provider: ISourceProvider, outputFile: File, minBitRate: Int, isRecordAudio: Boolean = true) {
143 | VRLogger.d("start() called with: isStarted = $isVideoStarted, isRunning = $isRunning, outputFile=$outputFile, minBitRate=$minBitRate")
144 | setUp(provider, outputFile, minBitRate, isRecordAudio)
145 | start()
146 | }
147 |
148 | /**
149 | * 更轻便
150 | * @param width 为null时,使用view的原始宽高
151 | * @param onResult 最简洁的方式下可以用lambda表达式拿结果
152 | */
153 | fun start(
154 | window: Window,
155 | view: View,
156 | outputFile: File? = null,
157 | width: Int? = null,
158 | minBitRate: Int = 1024_000,
159 | isRecordAudio: Boolean = true,
160 | onResult: (isSuccessful: Boolean, result: String) -> Unit,
161 | ) {
162 | VRLogger.d("start2() called with: isStarted = $isVideoStarted, isRunning = $isRunning, outputFile=$outputFile, minBitRate=$minBitRate")
163 | val provider = object : ISourceProvider {
164 | override fun next(): Bitmap {
165 | return RecordViewUtil.getBitmapFromView(window, view, width)
166 | }
167 |
168 | override fun onResult(isSuccessful: Boolean, result: String) {
169 | VRLogger.i("start2 onResult() isSuccessful: $isSuccessful, result: $result")
170 | onResult.invoke(isSuccessful, result)
171 | }
172 | }
173 | // 确认文件路径可用性
174 | var finalOutputFile =
175 | outputFile ?: File(view.context.externalCacheDir, "record_${System.currentTimeMillis()}.mp4")
176 | try {
177 | if (finalOutputFile.exists()) {
178 | if (finalOutputFile.isFile) {
179 | if (!finalOutputFile.delete()) {
180 | finalOutputFile = File(view.context.externalCacheDir, "record_${System.nanoTime()}.mp4")
181 | }
182 | } else {
183 | finalOutputFile = File(view.context.externalCacheDir, "record_${System.nanoTime()}.mp4")
184 | }
185 | }
186 | } catch (e: Exception) {
187 | e.printStackTrace()
188 | }
189 | setUp(
190 | provider = provider,
191 | outputFile = finalOutputFile,
192 | minBitRate = minBitRate,
193 | isRecordAudio = isRecordAudio,
194 | )
195 | start()
196 | }
197 |
198 | /**
199 | * 此处stop只是为了停止循环,真正的结束需要在循环的末尾,写入end标识到文件
200 | */
201 | fun stop() {
202 | if (!isVideoStarted) {
203 | VRLogger.d("stop() called 未启动,不用停止")
204 | return
205 | }
206 | VRLogger.i("stop() called")
207 | isRunning = false
208 | }
209 |
210 | @Synchronized
211 | private fun finish() {
212 | try {
213 | if (isMuxerStarted) {
214 | isMuxerStarted = false
215 | if (::mMediaMuxer.isInitialized) {
216 | mMediaMuxer.stop()
217 | mMediaMuxer.release()
218 | }
219 | VRLogger.i("finish() called, MediaMuxer release")
220 | }
221 | onResult(true, mOutputFile.absolutePath)
222 | } catch (e: Exception) {
223 | e.printStackTrace()
224 | onResult(false, "录制结束失败:${e.message}")
225 | } finally {
226 | isRunning = false
227 | }
228 | }
229 |
230 | @RequiresPermission(android.Manifest.permission.RECORD_AUDIO, conditional = true)
231 | @Throws
232 | private fun init(width: Int, height: Int, minBitRate: Int) {
233 | initVideoConfig(width, height, minBitRate)
234 | // Create the generated MP4 initialization object
235 | if (mOutputFile.exists()) {
236 | mOutputFile.delete()
237 | }
238 | mOutputFile.createNewFile()
239 | mMediaMuxer = MediaMuxer(mOutputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
240 | VRLogger.i("初始化完成, 最终参数, outputFile=${mOutputFile.absolutePath}, can write=[${mOutputFile.canWrite()}]")
241 | }
242 |
243 | private fun initVideoConfig(width: Int, height: Int, minBitRate: Int) {
244 | setColorFormat()
245 | mVideoRecordConfig = VideoRecordConfig(
246 | videoMimeType = videoMimeType,
247 | colorFormat = mColorFormat,
248 | outWidth = width,
249 | outHeight = height,
250 | bitRate = minBitRate,
251 | frameRate = frameRate,
252 | iFrameInterval = iFrameInterval
253 | )
254 | mVideoRecordConfig.videoMediaCodec.setCallback(object : MediaCodec.Callback() {
255 | override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) {
256 | VRLogger.d("视频Codec: onInputBufferAvailable: inputBufferId=$inputBufferId")
257 | try {
258 | processInput(codec, inputBufferId)
259 | } catch (e: Exception) {
260 | VRLogger.e("processInput错误", e)
261 | }
262 | }
263 |
264 | override fun onOutputBufferAvailable(codec: MediaCodec, outputBufferId: Int, info: MediaCodec.BufferInfo) {
265 | VRLogger.d("视频Codec: onOutputBufferAvailable: outputBufferId=$outputBufferId, info=$info")
266 | try {
267 | processOutput(codec, outputBufferId, info)
268 | } catch (e: Exception) {
269 | VRLogger.e("processOutput错误", e)
270 | }
271 | }
272 |
273 | override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
274 | VRLogger.e("视频Codec错误", e)
275 | //onError("视频录制失败:${e.message}")
276 | }
277 |
278 | override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
279 | // 打印信息日志
280 | VRLogger.d("视频Codec: onOutputFormatChanged: $format")
281 | processOutputFormatChanged(codec, format)
282 | }
283 | })
284 | mVideoRecordConfig.videoMediaCodec.start()
285 | }
286 |
287 | /**
288 | * 处理格式变动
289 | */
290 | private fun processOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
291 | // 从format中获取trackIndex
292 | val trackIndex = mMediaMuxer.addTrack(format)
293 | if (trackIndex >= 0) {
294 | mVideoRecordConfig.videoTrackIndex = trackIndex
295 | startMuxer()
296 | }
297 | }
298 |
299 | /**
300 | * 处理输入的帧
301 | */
302 | @Throws
303 | private fun processInput(codec: MediaCodec, inputBufferId: Int) {
304 | val inputBuffer = codec.getInputBuffer(inputBufferId) ?: return
305 | VRLogger.d("inputBuffer isDirect=${inputBuffer.isDirect}")
306 | inputBuffer.clear()
307 | // 计算pts,取帧的时间(getCurrentPixelsData)
308 | val ptsUsec = (System.nanoTime() - mRecordStartTime) / 1000
309 | VRLogger.v("视频pts=${ptsUsec}us")
310 | // 获取屏幕数据
311 | val inputData: ByteArray = getCurrentPixelsData()
312 | // todo 第一次changed 待修改 buffer is inaccessible
313 | inputBuffer.put(inputData)
314 | // 压入缓冲区,准备编码
315 | codec.queueInputBuffer(inputBufferId, 0, inputData.size, ptsUsec, 0)
316 | }
317 |
318 | /**
319 | * 处理输出帧到文件
320 | */
321 | @Throws
322 | private fun processOutput(codec: MediaCodec, outputBufferId: Int, info: MediaCodec.BufferInfo) {
323 | val outputBuffer = codec.getOutputBuffer(outputBufferId) ?: return
324 | val bufferFormat = codec.getOutputFormat(outputBufferId)
325 | try {
326 | outputBuffer.position(info.offset)
327 | outputBuffer.limit(info.offset + info.size)
328 | write(mVideoRecordConfig.videoTrackIndex, outputBuffer, info)
329 | // 用作记录
330 | mVideoRecordConfig.generateVideoFrameIndex++
331 | // 释放
332 | codec.releaseOutputBuffer(outputBufferId, false)
333 | } catch (e: Exception) {
334 | e.printStackTrace()
335 | }
336 | }
337 |
338 | /**
339 | * 写入文件
340 | */
341 | private fun write(track: Int, byteBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
342 | try {
343 | mMediaMuxer.writeSampleData(track, byteBuffer, info)
344 | } catch (e: IllegalStateException) {
345 | VRLogger.w("Write error", e)
346 | } catch (e: IllegalArgumentException) {
347 | VRLogger.w("Write error", e)
348 | }
349 | }
350 |
351 | private fun runVideo() {
352 | try {
353 | recordVideo()
354 | } catch (e: Exception) {
355 | VRLogger.e("视频录制错误", e)
356 | onError("视频录制失败:${e.message}")
357 | } finally {
358 | mVideoRecordConfig.destroy()
359 | finish()
360 | }
361 | }
362 |
363 | /**
364 | * 干脆就丢掉第一帧,没多大影响,可以简化代码流程
365 | */
366 | private fun recordVideo() {
367 |
368 | }
369 |
370 | /**
371 | * 从源提取像素数据
372 | */
373 | private fun getCurrentPixelsData(): ByteArray {
374 | val start = System.currentTimeMillis()
375 | // 这一步10ms左右
376 | val bitmap = mSourceProvider.next()
377 | VRLogger.v("提取完bitmap, size=${bitmap.byteCount / 1024}KB, 耗时=${System.currentTimeMillis() - start}ms")
378 | // 需要时间,400宽的都要10ms左右,1024*1024 S9耗时50ms左右,如果异步按帧率取,内存可能会爆炸, 800*800耗时21ms
379 | val inputData: ByteArray = EncoderTools.getPixels(
380 | mVideoRecordConfig.colorFormat,
381 | mVideoRecordConfig.outWidth,
382 | mVideoRecordConfig.outHeight,
383 | bitmap
384 | )
385 | VRLogger.v("从bitmap提取像素 ${System.currentTimeMillis() - start}ms")
386 | bitmap.recycle()
387 | return inputData
388 | }
389 |
390 | private fun runAudio() {
391 |
392 | }
393 |
394 | private fun recordAudio() {
395 |
396 | }
397 |
398 | private fun startMuxer() {
399 | if (!isMuxerStarted) {
400 | mMediaMuxer.start()
401 | isMuxerStarted = true
402 | VRLogger.i("MediaMuxer start, VideoTrackIndex=${mVideoRecordConfig.videoTrackIndex}")
403 | }
404 | }
405 |
406 | private fun setColorFormat() {
407 | mColorFormat = EncoderTools.getColorFormat()
408 | }
409 |
410 | private fun onError(message: String) {
411 | stop()
412 | onResult(false, message)
413 | }
414 |
415 | @Synchronized
416 | private fun onResult(isSuccessful: Boolean, result: String) {
417 | if (isResulted) {
418 | return
419 | }
420 | isResulted = true
421 | mSourceProvider.onResult(isSuccessful, result)
422 | }
423 | }
424 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/VideoRecordConfig.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record
2 |
3 | import android.media.MediaCodec
4 | import android.media.MediaFormat
5 | import io.keyss.view_record.utils.VRLogger
6 | import kotlin.math.max
7 |
8 | data class VideoRecordConfig(
9 | /** 编码类型 */
10 | val videoMimeType: String,
11 | val colorFormat: Int,
12 | var outWidth: Int,
13 | var outHeight: Int,
14 | /** 最终实际采用的比特率 */
15 | var bitRate: Int,
16 | /** 实际视频是动态帧率 */
17 | val frameRate: Float,
18 | /** I帧间隔:秒 */
19 | val iFrameInterval: Float,
20 | ) {
21 | val videoMediaCodec: MediaCodec
22 | var videoTrackIndex: Int = -1
23 | var generateVideoFrameIndex: Long = 0
24 |
25 | init {
26 | if (outWidth % 2 != 0) {
27 | outWidth -= 1
28 | }
29 | if (outHeight % 2 != 0) {
30 | outHeight -= 1
31 | }
32 |
33 | // config
34 | // acv h264
35 | //val mediaFormat = MediaFormat.createVideoFormat(mRecordMediaFormat, mOutWidth, mOutHeight)
36 | val mediaFormat = MediaFormat.createVideoFormat(videoMimeType, outWidth, outHeight)
37 | mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
38 | // 码率至少给个256Kbps吧
39 | bitRate = max(outWidth * outHeight, bitRate)
40 | mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
41 | mediaFormat.setFloat(MediaFormat.KEY_FRAME_RATE, frameRate)
42 | // 关键帧,单位居然是秒,25开始可以float
43 | mediaFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
44 | videoMediaCodec = MediaCodec.createEncoderByType(videoMimeType)
45 | videoMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
46 | VRLogger.d(
47 | "initVideoConfig: fps=$frameRate, BitRate=$bitRate, outputFormat=${videoMediaCodec.outputFormat}, width = $outWidth, height = $outHeight"
48 | )
49 | }
50 |
51 | fun destroy() {
52 | try {
53 | videoMediaCodec.stop()
54 | videoMediaCodec.release()
55 | } catch (e: Exception) {
56 | e.printStackTrace()
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/AudioEncoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.audio;
18 |
19 | import android.media.MediaCodec;
20 | import android.media.MediaCodecInfo;
21 | import android.media.MediaFormat;
22 | import android.util.Log;
23 |
24 | import androidx.annotation.NonNull;
25 |
26 | import java.nio.ByteBuffer;
27 | import java.util.List;
28 |
29 | import io.keyss.view_record.base.BaseEncoder;
30 | import io.keyss.view_record.base.Frame;
31 | import io.keyss.view_record.utils.CodecUtil;
32 |
33 | /**
34 | * Created by pedro on 19/01/17.
35 | *
36 | * Encode PCM audio data to ACC and return in a callback
37 | */
38 |
39 | public class AudioEncoder extends BaseEncoder {
40 |
41 | private final GetAacData getAacData;
42 | private int bitRate = 192 * 1024; //in kbps
43 | private int sampleRate = 44100; //in hz
44 | private int maxInputSize = 0;
45 | private boolean isStereo = true;
46 |
47 | public AudioEncoder(GetAacData getAacData) {
48 | this.getAacData = getAacData;
49 | TAG = "AudioEncoder";
50 | }
51 |
52 | /**
53 | * Prepare encoder with custom parameters
54 | */
55 | public boolean prepareAudioEncoder(int bitRate, int sampleRate, boolean isStereo, int maxInputSize) {
56 | this.bitRate = bitRate;
57 | this.sampleRate = sampleRate;
58 | this.maxInputSize = maxInputSize;
59 | this.isStereo = isStereo;
60 | try {
61 | MediaCodecInfo encoder = chooseEncoder(CodecUtil.AAC_MIME);
62 | if (encoder != null) {
63 | Log.i(TAG, "Audio Encoder selected " + encoder.getName());
64 | codec = MediaCodec.createByCodecName(encoder.getName());
65 | } else {
66 | Log.e(TAG, "Valid encoder not found");
67 | return false;
68 | }
69 |
70 | int channelCount = (isStereo) ? 2 : 1;
71 | MediaFormat audioFormat = MediaFormat.createAudioFormat(CodecUtil.AAC_MIME, sampleRate, channelCount);
72 | audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
73 | audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
74 | audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
75 | setCallback();
76 | codec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
77 | running = false;
78 | Log.i(TAG, "prepared");
79 | prepared = true;
80 | return true;
81 | } catch (Exception e) {
82 | Log.e(TAG, "Create AudioEncoder failed.", e);
83 | this.stop();
84 | return false;
85 | }
86 | }
87 |
88 | /**
89 | * Prepare encoder with default parameters
90 | */
91 | public boolean prepareAudioEncoder() {
92 | return prepareAudioEncoder(bitRate, sampleRate, isStereo, maxInputSize);
93 | }
94 |
95 | @Override
96 | public void start(boolean resetTs) {
97 | shouldReset = resetTs;
98 | Log.i(TAG, "started");
99 | }
100 |
101 | @Override
102 | protected void stopImp() {
103 | Log.i(TAG, "stopped");
104 | }
105 |
106 | @Override
107 | public void reset() {
108 | stop(false);
109 | prepareAudioEncoder(bitRate, sampleRate, isStereo, maxInputSize);
110 | restart();
111 | }
112 |
113 | @Override
114 | protected Frame getInputFrame() throws InterruptedException {
115 | return queue.take();
116 | }
117 |
118 | @Override
119 | protected long calculatePts(Frame frame, long presentTimeUs) {
120 | return Math.max(0, frame.getTimeStamp() - presentTimeUs);
121 | // return frame.getTimeStamp();
122 | }
123 |
124 | @Override
125 | protected void checkBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
126 | fixTimeStamp(bufferInfo);
127 | }
128 |
129 | @Override
130 | protected void sendBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
131 | getAacData.getAacData(byteBuffer, bufferInfo);
132 | }
133 |
134 | /**
135 | * Set custom PCM data.
136 | * Use it after prepareAudioEncoder(int sampleRate, int channel).
137 | * Used too with microphone.
138 | */
139 | public void inputPCMData(@NonNull Frame frame) {
140 | if (running && !queue.offer(frame)) {
141 | Log.i(TAG, "frame discarded");
142 | }
143 | }
144 |
145 | @Override
146 | protected MediaCodecInfo chooseEncoder(String mime) {
147 | List mediaCodecInfoList;
148 | if (force == CodecUtil.Force.HARDWARE) {
149 | mediaCodecInfoList = CodecUtil.getAllHardwareEncoders(CodecUtil.AAC_MIME);
150 | } else if (force == CodecUtil.Force.SOFTWARE) {
151 | mediaCodecInfoList = CodecUtil.getAllSoftwareEncoders(CodecUtil.AAC_MIME);
152 | } else {
153 | //Priority: hardware > software
154 | mediaCodecInfoList = CodecUtil.getAllEncoders(CodecUtil.AAC_MIME, true);
155 | }
156 |
157 | Log.i(TAG, mediaCodecInfoList.size() + " audio encoders found");
158 | if (mediaCodecInfoList.isEmpty()) return null;
159 | else return mediaCodecInfoList.get(0);
160 | }
161 |
162 | public void setSampleRate(int sampleRate) {
163 | this.sampleRate = sampleRate;
164 | }
165 |
166 | @Override
167 | public void formatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {
168 | getAacData.onAudioFormat(mediaFormat);
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/AudioPostProcessEffect.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.audio
17 |
18 | import android.media.audiofx.AcousticEchoCanceler
19 | import android.media.audiofx.AutomaticGainControl
20 | import android.media.audiofx.NoiseSuppressor
21 | import android.util.Log
22 |
23 | /**
24 | * Created by pedro on 11/05/17.
25 | * 音频二次加工的类
26 | */
27 | class AudioPostProcessEffect(private val microphoneId: Int) {
28 | private val TAG = "AudioPostProcessEffect"
29 | private var acousticEchoCanceler: AcousticEchoCanceler? = null
30 | private var automaticGainControl: AutomaticGainControl? = null
31 | private var noiseSuppressor: NoiseSuppressor? = null
32 |
33 | fun enableAutoGainControl() {
34 | if (AutomaticGainControl.isAvailable() && automaticGainControl == null) {
35 | automaticGainControl = AutomaticGainControl.create(microphoneId)
36 | automaticGainControl?.apply {
37 | enabled = true
38 | Log.i(TAG, "AutoGainControl enabled")
39 | } ?: run {
40 | Log.e(TAG, "This device doesn't implement AutoGainControl")
41 | }
42 | }
43 | }
44 |
45 | private fun releaseAutoGainControl() {
46 | automaticGainControl?.enabled = false
47 | automaticGainControl?.release()
48 | automaticGainControl = null
49 | }
50 |
51 | fun enableEchoCanceler() {
52 | if (AcousticEchoCanceler.isAvailable() && acousticEchoCanceler == null) {
53 | acousticEchoCanceler = AcousticEchoCanceler.create(microphoneId)
54 | acousticEchoCanceler?.apply {
55 | enabled = true
56 | Log.i(TAG, "EchoCanceler enabled")
57 | } ?: run {
58 | Log.e(TAG, "This device doesn't implement EchoCanceler")
59 | }
60 | }
61 | }
62 |
63 | private fun releaseEchoCanceler() {
64 | acousticEchoCanceler?.enabled = false
65 | acousticEchoCanceler?.release()
66 | acousticEchoCanceler = null
67 | }
68 |
69 | fun enableNoiseSuppressor() {
70 | if (NoiseSuppressor.isAvailable() && noiseSuppressor == null) {
71 | noiseSuppressor = NoiseSuppressor.create(microphoneId)
72 | noiseSuppressor?.apply {
73 | enabled = true
74 | Log.i(TAG, "NoiseSuppressor enabled")
75 | } ?: run {
76 | Log.e(TAG, "This device doesn't implement NoiseSuppressor")
77 | }
78 | }
79 | }
80 |
81 | private fun releaseNoiseSuppressor() {
82 | noiseSuppressor?.enabled = false
83 | noiseSuppressor?.release()
84 | noiseSuppressor = null
85 | }
86 |
87 | fun release() {
88 | releaseAutoGainControl()
89 | releaseEchoCanceler()
90 | releaseNoiseSuppressor()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/CustomAudioEffect.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.audio
17 |
18 | abstract class CustomAudioEffect {
19 | /**
20 | * @param pcmBuffer buffer obtained directly from the microphone.
21 | * @return it must be of same size that pcmBuffer parameter.
22 | */
23 | abstract fun process(pcmBuffer: ByteArray): ByteArray
24 | }
25 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/GetAacData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.audio
17 |
18 | import android.media.MediaCodec
19 | import android.media.MediaFormat
20 | import java.nio.ByteBuffer
21 |
22 | /**
23 | * Created by pedro on 19/01/17.
24 | */
25 | interface GetAacData {
26 | fun getAacData(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo)
27 | fun onAudioFormat(mediaFormat: MediaFormat)
28 | }
29 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/GetMicrophoneData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.audio
17 |
18 | import io.keyss.view_record.base.Frame
19 |
20 | /**
21 | * Created by pedro on 19/01/17.
22 | */
23 | interface GetMicrophoneData {
24 | fun inputPCMData(frame: Frame)
25 | }
26 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/MicrophoneManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.audio;
18 |
19 | import android.annotation.SuppressLint;
20 | import android.media.AudioFormat;
21 | import android.media.AudioRecord;
22 | import android.media.MediaRecorder;
23 | import android.os.Handler;
24 | import android.os.HandlerThread;
25 | import android.util.Log;
26 |
27 | import io.keyss.view_record.base.Frame;
28 |
29 | /**
30 | * Created by pedro on 19/01/17.
31 | */
32 |
33 | @SuppressLint("MissingPermission")
34 | public class MicrophoneManager {
35 | private final String TAG = "MicrophoneManager";
36 | private int BUFFER_SIZE = 0;
37 | protected AudioRecord audioRecord;
38 | private final GetMicrophoneData getMicrophoneData;
39 | protected byte[] pcmBuffer = new byte[BUFFER_SIZE];
40 | /**
41 | * 空白的,静音用
42 | */
43 | protected byte[] pcmBufferMuted = new byte[BUFFER_SIZE];
44 | protected boolean running = false;
45 | private boolean created = false;
46 | //default parameters for microphone
47 | private int sampleRate = 44100; //hz
48 | private final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
49 | private int channel = AudioFormat.CHANNEL_IN_STEREO;
50 | protected boolean muted = false;
51 | private AudioPostProcessEffect audioPostProcessEffect;
52 | protected HandlerThread handlerThread;
53 | protected CustomAudioEffect customAudioEffect = new NoAudioEffect();
54 |
55 | public MicrophoneManager(GetMicrophoneData getMicrophoneData) {
56 | this.getMicrophoneData = getMicrophoneData;
57 | }
58 |
59 | public void setCustomAudioEffect(CustomAudioEffect customAudioEffect) {
60 | this.customAudioEffect = customAudioEffect;
61 | }
62 |
63 | /**
64 | * Create audio record
65 | */
66 | public boolean createMicrophone() {
67 | boolean isSuccess = createMicrophone(sampleRate, true, false, false);
68 | Log.i(TAG, "Microphone created, " + sampleRate + "hz, Stereo");
69 | return isSuccess;
70 | }
71 |
72 | /**
73 | * Create audio record with params and default audio source
74 | */
75 | public boolean createMicrophone(int sampleRate, boolean isStereo, boolean echoCanceler,
76 | boolean noiseSuppressor) {
77 | return createMicrophone(MediaRecorder.AudioSource.DEFAULT, sampleRate, isStereo, echoCanceler,
78 | noiseSuppressor);
79 | }
80 |
81 | /**
82 | * Create audio record with params and selected audio source
83 | *
84 | * @param audioSource - the recording source. See {@link MediaRecorder.AudioSource} for the
85 | * recording source definitions.
86 | * @param echoCanceler 回音消除
87 | * @param noiseSuppressor 噪声抑制
88 | */
89 | public boolean createMicrophone(int audioSource, int sampleRate, boolean isStereo,
90 | boolean echoCanceler, boolean noiseSuppressor) {
91 | try {
92 | this.sampleRate = sampleRate;
93 | channel = isStereo ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO;
94 | // 计算buffer大小
95 | setPcmBufferSize(sampleRate, channel);
96 | audioRecord = new AudioRecord(audioSource, sampleRate, channel, audioFormat, getInputBufferSize());
97 | audioPostProcessEffect = new AudioPostProcessEffect(audioRecord.getAudioSessionId());
98 | if (echoCanceler) audioPostProcessEffect.enableEchoCanceler();
99 | if (noiseSuppressor) audioPostProcessEffect.enableNoiseSuppressor();
100 | String chl = (isStereo) ? "Stereo" : "Mono";
101 | if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
102 | throw new IllegalArgumentException("Some parameters specified are not valid");
103 | }
104 | Log.i(TAG, "Microphone created, " + sampleRate + "hz, " + chl);
105 | created = true;
106 | } catch (IllegalArgumentException e) {
107 | Log.e(TAG, "create microphone error", e);
108 | }
109 | return created;
110 | }
111 |
112 | /**
113 | * Start record and get data
114 | */
115 | public synchronized void start() {
116 | init();
117 | handlerThread = new HandlerThread(TAG);
118 | handlerThread.start();
119 | Handler handler = new Handler(handlerThread.getLooper());
120 | handler.post(() -> {
121 | while (running) {
122 | Frame frame = read();
123 | if (frame != null) {
124 | getMicrophoneData.inputPCMData(frame);
125 | }
126 | }
127 | });
128 | }
129 |
130 | private void init() {
131 | if (audioRecord != null) {
132 | audioRecord.startRecording();
133 | running = true;
134 | Log.i(TAG, "Microphone started");
135 | } else {
136 | throw new IllegalStateException("Error starting, microphone was stopped or not created, use createMicrophone() before start()");
137 | }
138 | }
139 |
140 | public void mute() {
141 | muted = true;
142 | }
143 |
144 | public void unMute() {
145 | muted = false;
146 | }
147 |
148 | public boolean isMuted() {
149 | return muted;
150 | }
151 |
152 | /**
153 | * @return Object with size and PCM buffer data
154 | */
155 | protected Frame read() {
156 | long timeStamp = System.nanoTime() / 1000;
157 | int size = audioRecord.read(pcmBuffer, 0, pcmBuffer.length);
158 | if (size < 0) {
159 | Log.e(TAG, "read error: " + size);
160 | return null;
161 | }
162 | return new Frame(muted ? pcmBufferMuted : customAudioEffect.process(pcmBuffer), 0, size, timeStamp);
163 | }
164 |
165 | /**
166 | * Stop and release microphone
167 | */
168 | public synchronized void stop() {
169 | running = false;
170 | created = false;
171 | if (handlerThread != null) {
172 | handlerThread.quitSafely();
173 | }
174 | if (audioRecord != null) {
175 | audioRecord.setRecordPositionUpdateListener(null);
176 | audioRecord.stop();
177 | audioRecord.release();
178 | audioRecord = null;
179 | }
180 | if (audioPostProcessEffect != null) {
181 | audioPostProcessEffect.release();
182 | }
183 | Log.i(TAG, "Microphone stopped");
184 | }
185 |
186 | /**
187 | * Get PCM buffer size
188 | */
189 | private void setPcmBufferSize(int sampleRate, int channel) {
190 | BUFFER_SIZE = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat);
191 | pcmBuffer = new byte[BUFFER_SIZE];
192 | pcmBufferMuted = new byte[BUFFER_SIZE];
193 | }
194 |
195 | public int getInputBufferSize() {
196 | return BUFFER_SIZE;
197 | }
198 |
199 | public int getSampleRate() {
200 | return sampleRate;
201 | }
202 |
203 | public void setSampleRate(int sampleRate) {
204 | this.sampleRate = sampleRate;
205 | }
206 |
207 | public int getAudioFormat() {
208 | return audioFormat;
209 | }
210 |
211 | public int getChannel() {
212 | return channel;
213 | }
214 |
215 | public boolean isRunning() {
216 | return running;
217 | }
218 |
219 | public boolean isCreated() {
220 | return created;
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/audio/NoAudioEffect.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.audio
17 |
18 | class NoAudioEffect : CustomAudioEffect() {
19 | override fun process(pcmBuffer: ByteArray): ByteArray {
20 | return pcmBuffer
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/base/BaseEncoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.base;
18 |
19 | import android.media.MediaCodec;
20 | import android.media.MediaCodecInfo;
21 | import android.media.MediaFormat;
22 | import android.os.Handler;
23 | import android.os.HandlerThread;
24 | import android.util.Log;
25 |
26 | import androidx.annotation.NonNull;
27 |
28 | import java.nio.ByteBuffer;
29 | import java.util.concurrent.ArrayBlockingQueue;
30 | import java.util.concurrent.BlockingQueue;
31 |
32 | import io.keyss.view_record.utils.CodecUtil;
33 | import io.keyss.view_record.video.EncoderCallback;
34 | import io.keyss.view_record.video.EncoderErrorCallback;
35 | import io.keyss.view_record.video.IFrameDataGetter;
36 |
37 | /**
38 | * Created by pedro on 18/09/19.
39 | */
40 | public abstract class BaseEncoder implements EncoderCallback {
41 | protected String TAG = "BaseEncoder";
42 | private final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
43 | private HandlerThread handlerThread;
44 | // 持续输入数据的队列
45 | protected BlockingQueue queue = new ArrayBlockingQueue<>(80);
46 | // 实时型
47 | protected boolean isRealTime = false;
48 | // 获取当前数据帧接口
49 | protected IFrameDataGetter iFrameDataGetter;
50 | protected MediaCodec codec;
51 | protected static long presentTimeUs;
52 | protected volatile boolean running = false;
53 | protected CodecUtil.Force force = CodecUtil.Force.FIRST_COMPATIBLE_FOUND;
54 | private MediaCodec.Callback callback;
55 | private long oldTimeStamp = 0L;
56 | protected boolean shouldReset = true;
57 | protected boolean prepared = false;
58 | private Handler handler;
59 | private EncoderErrorCallback encoderErrorCallback;
60 |
61 | public void setEncoderErrorCallback(EncoderErrorCallback encoderErrorCallback) {
62 | this.encoderErrorCallback = encoderErrorCallback;
63 | }
64 |
65 | public void setFrameDataGetter(IFrameDataGetter iFrameDataGetter) {
66 | isRealTime = true;
67 | this.iFrameDataGetter = iFrameDataGetter;
68 | }
69 |
70 | public void setRealTime(boolean realTime) {
71 | isRealTime = realTime;
72 | }
73 |
74 | public void restart() {
75 | start(false);
76 | initCodec();
77 | }
78 |
79 | public void start() {
80 | if (!prepared)
81 | throw new IllegalStateException(TAG + " not prepared yet. You must call prepare method before start it");
82 | if (presentTimeUs == 0) {
83 | presentTimeUs = System.nanoTime() / 1000;
84 | }
85 | start(true);
86 | initCodec();
87 | }
88 |
89 | protected void setCallback() {
90 | handlerThread = new HandlerThread(TAG);
91 | handlerThread.start();
92 | handler = new Handler(handlerThread.getLooper());
93 | createAsyncCallback();
94 | codec.setCallback(callback, handler);
95 | }
96 |
97 | private void initCodec() {
98 | // reset的时候可能出现
99 | if (codec == null) {
100 | throw new IllegalStateException("codec未初始化");
101 | }
102 | codec.start();
103 | running = true;
104 | }
105 |
106 | public abstract void reset();
107 |
108 | public abstract void start(boolean resetTs);
109 |
110 | protected abstract void stopImp();
111 |
112 | protected void fixTimeStamp(MediaCodec.BufferInfo info) {
113 | if (oldTimeStamp > info.presentationTimeUs) {
114 | info.presentationTimeUs = oldTimeStamp;
115 | } else {
116 | oldTimeStamp = info.presentationTimeUs;
117 | }
118 | }
119 |
120 | private void reloadCodec(IllegalStateException e) {
121 | //Sometimes encoder crash, we will try recover it. Reset encoder a time if crash
122 | EncoderErrorCallback callback = encoderErrorCallback;
123 | if (callback != null) {
124 | shouldReset = callback.onEncodeError(TAG, e);
125 | }
126 | if (shouldReset) {
127 | Log.e(TAG, "Encoder crashed, trying to recover it", e);
128 | reset();
129 | }
130 | }
131 |
132 | public void stop() {
133 | stop(true);
134 | }
135 |
136 | public void stop(boolean resetTs) {
137 | if (resetTs) {
138 | presentTimeUs = 0;
139 | }
140 | running = false;
141 | stopImp();
142 | if (handlerThread != null) {
143 | if (handlerThread.getLooper() != null) {
144 | if (handlerThread.getLooper().getThread() != null) {
145 | handlerThread.getLooper().getThread().interrupt();
146 | }
147 | handlerThread.getLooper().quit();
148 | }
149 | handlerThread.quit();
150 | if (codec != null) {
151 | try {
152 | codec.flush();
153 | } catch (IllegalStateException ignored) {
154 | }
155 | }
156 | //wait for thread to die for 500ms.
157 | try {
158 | handlerThread.getLooper().getThread().join(500);
159 | } catch (Exception ignored) {
160 | }
161 | }
162 | queue.clear();
163 | queue = new ArrayBlockingQueue<>(80);
164 | try {
165 | codec.stop();
166 | codec.release();
167 | codec = null;
168 | } catch (Exception e) {
169 | Log.e(TAG, "Error stopping codec", e);
170 | } finally {
171 | codec = null;
172 | }
173 | prepared = false;
174 | oldTimeStamp = 0L;
175 | }
176 |
177 | protected abstract MediaCodecInfo chooseEncoder(String mime);
178 |
179 | protected abstract Frame getInputFrame() throws InterruptedException;
180 |
181 | protected abstract long calculatePts(Frame frame, long presentTimeUs);
182 |
183 | /**
184 | * 这个方法里多处耗时的地方,都要处理终止状态,否则会报个不大不小,毫无影响的异常,就是这个byteBuffer已经不可用了
185 | */
186 | private void processInput(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec mediaCodec,
187 | int inBufferIndex) throws IllegalStateException {
188 | byteBuffer.clear();
189 | // 试试,防止偷跑
190 | if (!running) {
191 | Log.d(TAG, "processInput1: not running");
192 | return;
193 | }
194 | try {
195 | Frame frame = getInputFrame();
196 | // 如果停止的时候返回null,那这里就会死循环,所以上一层不可以给null
197 | while (frame == null && running) frame = getInputFrame();
198 | // 在这里终止掉
199 | if (!running) {
200 | Log.d(TAG, "processInput3: not running");
201 | return;
202 | }
203 | int size = Math.max(0, Math.min(frame.getSize(), byteBuffer.remaining()) - frame.getOffset());
204 | byteBuffer.put(frame.getBuffer(), frame.getOffset(), size);
205 | long pts = calculatePts(frame, presentTimeUs);
206 | mediaCodec.queueInputBuffer(inBufferIndex, 0, size, pts, 0);
207 | } catch (InterruptedException e) {
208 | Thread.currentThread().interrupt();
209 | } catch (NullPointerException | IndexOutOfBoundsException e) {
210 | Log.i(TAG, "Encoding error", e);
211 | }
212 | }
213 |
214 | protected abstract void checkBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo);
215 |
216 | protected abstract void sendBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo);
217 |
218 | private void processOutput(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec mediaCodec,
219 | int outBufferIndex, @NonNull MediaCodec.BufferInfo bufferInfo) throws IllegalStateException {
220 | checkBuffer(byteBuffer, bufferInfo);
221 | sendBuffer(byteBuffer, bufferInfo);
222 | mediaCodec.releaseOutputBuffer(outBufferIndex, false);
223 | }
224 |
225 | public void setForce(CodecUtil.Force force) {
226 | this.force = force;
227 | }
228 |
229 | public boolean isRunning() {
230 | return running;
231 | }
232 |
233 | @Override
234 | public void inputAvailable(@NonNull MediaCodec mediaCodec, int inBufferIndex) throws IllegalStateException {
235 | // 试试,防止偷跑
236 | if (!running) {
237 | Log.d(TAG, "inputAvailable: not running");
238 | return;
239 | }
240 | ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inBufferIndex);
241 | processInput(byteBuffer, mediaCodec, inBufferIndex);
242 | }
243 |
244 | @Override
245 | public void outputAvailable(@NonNull MediaCodec mediaCodec, int outBufferIndex,
246 | @NonNull MediaCodec.BufferInfo bufferInfo) throws IllegalStateException {
247 | ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outBufferIndex);
248 | processOutput(byteBuffer, mediaCodec, outBufferIndex, bufferInfo);
249 | }
250 |
251 | private void createAsyncCallback() {
252 | callback = new MediaCodec.Callback() {
253 | @Override
254 | public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int inBufferIndex) {
255 | try {
256 | inputAvailable(mediaCodec, inBufferIndex);
257 | } catch (IllegalStateException e) {
258 | Log.w(TAG, "MediaCodec.Callback.onInputBufferAvailable Encoding error", e);
259 | reloadCodec(e);
260 | }
261 | }
262 |
263 | @Override
264 | public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int outBufferIndex, @NonNull MediaCodec.BufferInfo bufferInfo) {
265 | try {
266 | outputAvailable(mediaCodec, outBufferIndex, bufferInfo);
267 | } catch (IllegalStateException e) {
268 | Log.w(TAG, "MediaCodec.Callback.onOutputBufferAvailable Encoding error", e);
269 | reloadCodec(e);
270 | }
271 | }
272 |
273 | @Override
274 | public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
275 | Log.e(TAG, "MediaCodec.Callback.onError", e);
276 | EncoderErrorCallback callback = encoderErrorCallback;
277 | if (callback != null) callback.onCodecError(TAG, e);
278 | }
279 |
280 | @Override
281 | public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
282 | @NonNull MediaFormat mediaFormat) {
283 | formatChanged(mediaCodec, mediaFormat);
284 | }
285 | };
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/base/Frame.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.base
17 |
18 | /**
19 | * Created by pedro on 17/02/18.
20 | */
21 | class Frame {
22 | var buffer: ByteArray
23 | var offset: Int
24 | var size: Int
25 | var timeStamp: Long
26 |
27 | constructor(buffer: ByteArray, timeStamp: Long = System.nanoTime() / 1000) {
28 | this.buffer = buffer
29 | offset = 0
30 | size = buffer.size
31 | this.timeStamp = timeStamp
32 | }
33 |
34 | /**
35 | * Used with audio frame
36 | */
37 | constructor(buffer: ByteArray, offset: Int, size: Int, timeStamp: Long = System.nanoTime() / 1000) {
38 | this.buffer = buffer
39 | this.offset = offset
40 | this.size = size
41 | this.timeStamp = timeStamp
42 | }
43 |
44 | override fun toString(): String {
45 | return "Frame(offset=$offset, size=$size, timeStamp=$timeStamp)"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/recording/AndroidMuxerRecordController.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.recording;
18 |
19 | import android.media.MediaCodec;
20 | import android.media.MediaFormat;
21 | import android.media.MediaMuxer;
22 | import android.os.Build;
23 | import android.util.Log;
24 |
25 | import androidx.annotation.NonNull;
26 | import androidx.annotation.Nullable;
27 | import androidx.annotation.RequiresApi;
28 |
29 | import java.io.FileDescriptor;
30 | import java.io.IOException;
31 | import java.nio.ByteBuffer;
32 |
33 | /**
34 | * Created by pedro on 08/03/19.
35 | *
36 | * Class to control video recording with MediaMuxer.
37 | */
38 | public class AndroidMuxerRecordController extends BaseRecordController {
39 |
40 | private static final String TAG = "AndroidRecordController";
41 | private MediaMuxer mediaMuxer;
42 | private MediaFormat videoFormat, audioFormat;
43 |
44 | @Override
45 | public void startRecord(@NonNull String path, @Nullable Listener listener) throws IOException {
46 | mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
47 | this.listener = listener;
48 | status = Status.STARTED;
49 | if (listener != null) listener.onStatusChange(status);
50 | if (isOnlyAudio && audioFormat != null) init();
51 | }
52 |
53 | @Override
54 | @RequiresApi(api = Build.VERSION_CODES.O)
55 | public void startRecord(@NonNull FileDescriptor fd, @Nullable Listener listener) throws IOException {
56 | mediaMuxer = new MediaMuxer(fd, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
57 | this.listener = listener;
58 | status = Status.STARTED;
59 | if (listener != null) listener.onStatusChange(status);
60 | if (isOnlyAudio && audioFormat != null) init();
61 | }
62 |
63 | @Override
64 | public void stopRecord() {
65 | videoTrack = -1;
66 | audioTrack = -1;
67 | status = Status.STOPPED;
68 | if (mediaMuxer != null) {
69 | try {
70 | mediaMuxer.stop();
71 | mediaMuxer.release();
72 | } catch (Exception ignored) {
73 | }
74 | }
75 | mediaMuxer = null;
76 | pauseMoment = 0;
77 | pauseTime = 0;
78 | if (listener != null) listener.onStatusChange(status);
79 | }
80 |
81 | @Override
82 | public void recordVideo(ByteBuffer videoBuffer, MediaCodec.BufferInfo videoInfo) {
83 | if (status == Status.STARTED && videoFormat != null && (audioFormat != null || isOnlyVideo)) {
84 | if (videoInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME || isKeyFrame(videoBuffer)) {
85 | videoTrack = mediaMuxer.addTrack(videoFormat);
86 | init();
87 | }
88 | } else if (status == Status.RESUMED
89 | && (videoInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME || isKeyFrame(videoBuffer))) {
90 | status = Status.RECORDING;
91 | if (listener != null) listener.onStatusChange(status);
92 | }
93 | if (status == Status.RECORDING) {
94 | updateFormat(this.videoInfo, videoInfo);
95 | write(videoTrack, videoBuffer, this.videoInfo);
96 | }
97 | }
98 |
99 | @Override
100 | public void recordAudio(ByteBuffer audioBuffer, MediaCodec.BufferInfo audioInfo) {
101 | if (status == Status.RECORDING) {
102 | updateFormat(this.audioInfo, audioInfo);
103 | write(audioTrack, audioBuffer, this.audioInfo);
104 | }
105 | }
106 |
107 | @Override
108 | public void setVideoFormat(MediaFormat videoFormat, boolean isOnlyVideo) {
109 | this.videoFormat = videoFormat;
110 | this.isOnlyVideo = isOnlyVideo;
111 | }
112 |
113 | @Override
114 | public void setAudioFormat(MediaFormat audioFormat, boolean isOnlyAudio) {
115 | this.audioFormat = audioFormat;
116 | this.isOnlyAudio = isOnlyAudio;
117 | if (isOnlyAudio && status == Status.STARTED) {
118 | init();
119 | }
120 | }
121 |
122 | @Override
123 | public void resetFormats() {
124 | videoFormat = null;
125 | audioFormat = null;
126 | }
127 |
128 | private void init() {
129 | if (!isOnlyVideo) audioTrack = mediaMuxer.addTrack(audioFormat);
130 | mediaMuxer.start();
131 | status = Status.RECORDING;
132 | if (listener != null) listener.onStatusChange(status);
133 | }
134 |
135 | private void write(int track, ByteBuffer byteBuffer, MediaCodec.BufferInfo info) {
136 | try {
137 | mediaMuxer.writeSampleData(track, byteBuffer, info);
138 | } catch (IllegalStateException | IllegalArgumentException e) {
139 | Log.i(TAG, "Write error", e);
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/recording/BaseRecordController.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.recording;
18 |
19 | import android.media.MediaCodec;
20 | import android.media.MediaFormat;
21 |
22 | import java.nio.ByteBuffer;
23 |
24 | import io.keyss.view_record.utils.CodecUtil;
25 | import io.keyss.view_record.utils.FrameUtil;
26 |
27 | public abstract class BaseRecordController implements RecordController {
28 |
29 | protected Status status = Status.STOPPED;
30 | protected String videoMime = CodecUtil.H264_MIME;
31 | protected long pauseMoment = 0;
32 | protected long pauseTime = 0;
33 | protected Listener listener;
34 | protected int videoTrack = -1;
35 | protected int audioTrack = -1;
36 | protected final MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();
37 | protected final MediaCodec.BufferInfo audioInfo = new MediaCodec.BufferInfo();
38 | protected boolean isOnlyAudio = false;
39 | protected boolean isOnlyVideo = false;
40 |
41 | public void setVideoMime(String videoMime) {
42 | this.videoMime = videoMime;
43 | }
44 |
45 | public boolean isRunning() {
46 | return status == Status.STARTED
47 | || status == Status.RECORDING
48 | || status == Status.RESUMED
49 | || status == Status.PAUSED;
50 | }
51 |
52 | public boolean isRecording() {
53 | return status == Status.RECORDING;
54 | }
55 |
56 | public Status getStatus() {
57 | return status;
58 | }
59 |
60 | public void pauseRecord() {
61 | if (status == Status.RECORDING) {
62 | pauseMoment = System.nanoTime() / 1000;
63 | status = Status.PAUSED;
64 | if (listener != null) listener.onStatusChange(status);
65 | }
66 | }
67 |
68 | public void resumeRecord() {
69 | if (status == Status.PAUSED) {
70 | pauseTime += System.nanoTime() / 1000 - pauseMoment;
71 | status = Status.RESUMED;
72 | if (listener != null) listener.onStatusChange(status);
73 | }
74 | }
75 |
76 | protected boolean isKeyFrame(ByteBuffer videoBuffer) {
77 | byte[] header = new byte[5];
78 | videoBuffer.duplicate().get(header, 0, header.length);
79 | if (videoMime.equals(CodecUtil.H264_MIME) && (header[4] & 0x1F) == FrameUtil.IDR) { //h264
80 | return true;
81 | } else { //h265
82 | return videoMime.equals(CodecUtil.H265_MIME)
83 | && ((header[4] >> 1) & 0x3f) == FrameUtil.IDR_W_DLP
84 | || ((header[4] >> 1) & 0x3f) == FrameUtil.IDR_N_LP;
85 | }
86 | }
87 |
88 | //We can't reuse info because could produce stream issues
89 | protected void updateFormat(MediaCodec.BufferInfo newInfo, MediaCodec.BufferInfo oldInfo) {
90 | newInfo.flags = oldInfo.flags;
91 | newInfo.offset = oldInfo.offset;
92 | newInfo.size = oldInfo.size;
93 | newInfo.presentationTimeUs = oldInfo.presentationTimeUs - pauseTime;
94 | }
95 |
96 | public void setVideoFormat(MediaFormat videoFormat) {
97 | setVideoFormat(videoFormat, false);
98 | }
99 |
100 | public void setAudioFormat(MediaFormat audioFormat) {
101 | setAudioFormat(audioFormat, false);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/recording/RecordController.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.recording;
18 |
19 | import android.media.MediaCodec;
20 | import android.media.MediaFormat;
21 |
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.Nullable;
24 |
25 | import java.io.FileDescriptor;
26 | import java.io.IOException;
27 | import java.nio.ByteBuffer;
28 |
29 | public interface RecordController {
30 | void startRecord(@NonNull String path, @Nullable Listener listener) throws IOException;
31 |
32 | void startRecord(@NonNull FileDescriptor fd, @Nullable Listener listener) throws IOException;
33 |
34 | void stopRecord();
35 |
36 | void recordVideo(ByteBuffer videoBuffer, MediaCodec.BufferInfo videoInfo);
37 |
38 | void recordAudio(ByteBuffer audioBuffer, MediaCodec.BufferInfo audioInfo);
39 |
40 | void setVideoFormat(MediaFormat videoFormat, boolean isOnlyVideo);
41 |
42 | void setAudioFormat(MediaFormat audioFormat, boolean isOnlyAudio);
43 |
44 | void resetFormats();
45 |
46 | interface Listener {
47 | void onStatusChange(Status status);
48 | }
49 |
50 | enum Status {
51 | STARTED, STOPPED, RECORDING, PAUSED, RESUMED
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/recording/ViewRecorder.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.recording
2 |
3 | import android.media.AudioFormat
4 | import android.media.MediaCodec
5 | import android.media.MediaFormat
6 | import android.media.MediaRecorder
7 | import android.util.Log
8 | import android.view.View
9 | import android.view.Window
10 | import io.keyss.view_record.audio.AudioEncoder
11 | import io.keyss.view_record.audio.GetAacData
12 | import io.keyss.view_record.audio.GetMicrophoneData
13 | import io.keyss.view_record.audio.MicrophoneManager
14 | import io.keyss.view_record.base.Frame
15 | import io.keyss.view_record.recording.RecordController.Listener
16 | import io.keyss.view_record.utils.RecordViewUtil
17 | import io.keyss.view_record.utils.yuv.ConvertUtil
18 | import io.keyss.view_record.video.EncoderErrorCallback
19 | import io.keyss.view_record.video.FormatVideoEncoder
20 | import io.keyss.view_record.video.GetVideoData
21 | import io.keyss.view_record.video.IFrameDataGetter
22 | import io.keyss.view_record.video.VideoEncoder
23 | import java.nio.ByteBuffer
24 |
25 | /**
26 | * Description: 采用pedro版本的编码类进行录制的一个版本
27 | *
28 | * Time: 2023/11/21 18:52
29 | * @author Key
30 | */
31 | class ViewRecorder {
32 | companion object {
33 | private const val TAG = "ViewRecordEncoder"
34 | }
35 |
36 | private lateinit var view: View
37 | private lateinit var window: Window
38 |
39 | /**
40 | * 是否已启动
41 | */
42 | @Volatile
43 | var isStartRecord = false
44 | private set
45 |
46 | /** 视频编码器 */
47 | private lateinit var videoEncoder: VideoEncoder
48 | private var videoInitSuccess = false
49 |
50 | private var audioInitSuccess = false
51 | private lateinit var audioEncoder: AudioEncoder
52 | private lateinit var microphoneManager: MicrophoneManager
53 |
54 | private lateinit var recordController: AndroidMuxerRecordController
55 |
56 | /**
57 | * 只录视频时只初始化视频编码器
58 | */
59 | @Throws
60 | fun initJustVideo(
61 | window: Window,
62 | view: View,
63 | width: Int,
64 | fps: Int = 24,
65 | videoBitRate: Int = 4_000_000,
66 | iFrameInterval: Int = 1,
67 | ) {
68 | if (isStartRecord) {
69 | throw IllegalStateException("recording is running")
70 | }
71 | this.window = window
72 | this.view = view
73 | recordController = AndroidMuxerRecordController()
74 | videoEncoder = VideoEncoder(object : GetVideoData {
75 | override fun getVideoData(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
76 | //Log.d(TAG, "getVideoData() called with: h264Buffer = $h264Buffer, info = $info")
77 | //fpsListener.calculateFps()
78 | recordController.recordVideo(h264Buffer, info)
79 | }
80 |
81 | override fun onVideoFormat(mediaFormat: MediaFormat) {
82 | Log.d(TAG, "onVideoFormat() called with: mediaFormat = $mediaFormat")
83 | recordController.setVideoFormat(mediaFormat, !audioInitSuccess)
84 | }
85 | })
86 | // 设置获取帧的方法
87 | videoEncoder.setFrameDataGetter(object : IFrameDataGetter {
88 | override fun getFrameData(): Frame {
89 | return Frame(getFrameBytes())
90 | }
91 | })
92 | // 通过获取一帧来初始化视频参数
93 | val frameBitmap = getFrameBitmap(width)
94 | videoInitSuccess = videoEncoder.prepareVideoEncoder(
95 | frameBitmap.width,
96 | frameBitmap.height,
97 | fps,
98 | videoBitRate,
99 | iFrameInterval,
100 | FormatVideoEncoder.YUV420Dynamical
101 | // NOTE: 已知在就算支持的颜色格式中,也可能会出现oom,如YUV420_PLANAR在荣耀某款平板上
102 | //FormatVideoEncoder.YUV420_SEMI_PLANAR//21 V
103 | //FormatVideoEncoder.YUV420_PLANAR//19 V
104 | //FormatVideoEncoder.YUV420_PACKED_SEMI_PLANAR//39 V
105 | //FormatVideoEncoder.YUV420_PACKED_PLANAR//20 V
106 | )
107 | }
108 |
109 | /**
110 | * 初始化录制:view视频+mic音频
111 | */
112 | @Throws
113 | fun init(
114 | window: Window,
115 | view: View,
116 | width: Int,
117 | fps: Int = 24,
118 | videoBitRate: Int = 4_000_000,
119 | iFrameInterval: Int = 1,
120 | audioBitRate: Int = 192_000,
121 | audioSampleRate: Int = 44_100,
122 | isStereo: Boolean = true,
123 | ) {
124 | // 视频设置
125 | initJustVideo(window, view, width, fps, videoBitRate, iFrameInterval)
126 |
127 | // 音频设置
128 | audioEncoder = AudioEncoder(object : GetAacData {
129 | override fun getAacData(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
130 | recordController.recordAudio(aacBuffer, info)
131 | }
132 |
133 | override fun onAudioFormat(mediaFormat: MediaFormat) {
134 | recordController.setAudioFormat(mediaFormat)
135 | }
136 | })
137 | audioEncoder.setRealTime(true)
138 | //audioEncoder.setForce(CodecUtil.Force.SOFTWARE)
139 |
140 | microphoneManager = MicrophoneManager(object : GetMicrophoneData {
141 | override fun inputPCMData(frame: Frame) {
142 | audioEncoder.inputPCMData(frame)
143 | }
144 | })
145 |
146 | audioInitSuccess = microphoneManager.createMicrophone(
147 | MediaRecorder.AudioSource.DEFAULT,
148 | audioSampleRate,
149 | isStereo,
150 | false,
151 | false
152 | )
153 | if (!audioInitSuccess) {
154 | // 已失败,不再初始化音频编码器
155 | return
156 | }
157 | audioInitSuccess = audioEncoder.prepareAudioEncoder(
158 | audioBitRate,
159 | microphoneManager.sampleRate,
160 | microphoneManager.channel == AudioFormat.CHANNEL_IN_STEREO,
161 | microphoneManager.inputBufferSize
162 | )
163 | }
164 |
165 | @Throws
166 | fun startRecord(path: String, statusListener: Listener, errorListener: EncoderErrorCallback) {
167 | // 判断下是否已经初始化,及初始化是否成功
168 | if (!this::videoEncoder.isInitialized || !videoInitSuccess) {
169 | throw IllegalStateException("videoEncoder is not initialized, videoInitSuccess=$videoInitSuccess")
170 | }
171 | if (isStartRecord) {
172 | return
173 | }
174 | isStartRecord = true
175 | // 设置错误回调
176 | videoEncoder.setEncoderErrorCallback(errorListener)
177 | // 启动并设置正确的回调
178 | recordController.startRecord(path, statusListener)
179 | if (audioInitSuccess) {
180 | microphoneManager.start()
181 | audioEncoder.start()
182 | }
183 | videoEncoder.start()
184 | }
185 |
186 | fun stopRecord() {
187 | if (!isStartRecord) {
188 | return
189 | }
190 | isStartRecord = false
191 | recordController.stopRecord()
192 | if (!recordController.isRecording) {
193 | Log.i(TAG, "stopRecord() called not isRecording")
194 | videoEncoder.stop()
195 | if (audioInitSuccess) {
196 | audioEncoder.stop()
197 | microphoneManager.stop()
198 | }
199 | recordController.resetFormats()
200 | }
201 | videoInitSuccess = false
202 | audioInitSuccess = false
203 | }
204 |
205 | private fun getFrameBytes(): ByteArray {
206 | if (!this::view.isInitialized || !this::window.isInitialized) {
207 | throw IllegalStateException("view or window is not initialized")
208 | }
209 | //val start = System.currentTimeMillis()
210 | val bitmap = getFrameBitmap(videoEncoder.width)
211 | //val getBitmapCost = System.currentTimeMillis() - start
212 | val inputData: ByteArray = ConvertUtil.convertBitmapToYUVByteArray(
213 | bitmap,
214 | videoEncoder.formatVideoEncoder.formatCodec
215 | )
216 | //VRLogger.v("getFrameBytes() bitmap width=${videoEncoder.width}, colorFormat: ${videoEncoder.formatVideoEncoder.formatCodec}, getBitmapCost: ${getBitmapCost}ms, total cost: ${System.currentTimeMillis() - start}ms")
217 | return inputData
218 | }
219 |
220 | private fun getFrameBitmap(width: Int) = RecordViewUtil.getBitmapFromView(window, view, width)
221 | }
222 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/CodecUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.utils;
18 |
19 | import android.media.MediaCodecInfo;
20 | import android.media.MediaCodecList;
21 | import android.os.Build;
22 |
23 | import java.util.ArrayList;
24 | import java.util.Arrays;
25 | import java.util.List;
26 |
27 | /**
28 | * Created by pedro on 14/02/18.
29 | */
30 |
31 | public class CodecUtil {
32 |
33 | private static final String TAG = "CodecUtil";
34 |
35 | public static final String H264_MIME = "video/avc";
36 | public static final String H265_MIME = "video/hevc";
37 | public static final String AAC_MIME = "audio/mp4a-latm";
38 | public static final String VORBIS_MIME = "audio/ogg";
39 | public static final String OPUS_MIME = "audio/opus";
40 |
41 | public enum Force {
42 | FIRST_COMPATIBLE_FOUND, SOFTWARE, HARDWARE
43 | }
44 |
45 | public static List showAllCodecsInfo() {
46 | List mediaCodecInfoList = getAllCodecs(false);
47 | List infos = new ArrayList<>();
48 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
49 | StringBuilder info = new StringBuilder("----------------\n");
50 | info.append("Name: ")
51 | .append(mediaCodecInfo.getName())
52 | .append("\n");
53 | for (String type : mediaCodecInfo.getSupportedTypes()) {
54 | info.append("Type: ")
55 | .append(type)
56 | .append("\n");
57 | MediaCodecInfo.CodecCapabilities codecCapabilities =
58 | mediaCodecInfo.getCapabilitiesForType(type);
59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
60 | info.append("Max instances: ")
61 | .append(codecCapabilities.getMaxSupportedInstances())
62 | .append("\n");
63 | }
64 | if (mediaCodecInfo.isEncoder()) {
65 | info.append("----- Encoder info -----\n");
66 | MediaCodecInfo.EncoderCapabilities encoderCapabilities = null;
67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
68 | encoderCapabilities = codecCapabilities.getEncoderCapabilities();
69 | info.append("Complexity range: ")
70 | .append(encoderCapabilities.getComplexityRange().getLower())
71 | .append(" - ")
72 | .append(encoderCapabilities.getComplexityRange().getUpper())
73 | .append("\n");
74 | }
75 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
76 | info.append("Quality range: ")
77 | .append(encoderCapabilities.getQualityRange().getLower())
78 | .append(" - ")
79 | .append(encoderCapabilities.getQualityRange().getUpper())
80 | .append("\n");
81 | }
82 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
83 | info.append("CBR supported: ")
84 | .append(encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR))
85 | .append("\n")
86 | .append("VBR supported: ")
87 | .append(encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR))
88 | .append("\n")
89 | .append("CQ supported: ")
90 | .append(encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ))
91 | .append("\n");
92 | }
93 | info.append("----- -----\n");
94 | } else {
95 | info.append("----- Decoder info -----\n")
96 | .append("----- -----\n");
97 | }
98 |
99 | if (codecCapabilities.colorFormats != null && codecCapabilities.colorFormats.length > 0) {
100 | info.append("----- Video info -----\n")
101 | .append("Supported colors: \n");
102 | for (int color : codecCapabilities.colorFormats)
103 | info.append(color)
104 | .append("\n");
105 | for (MediaCodecInfo.CodecProfileLevel profile : codecCapabilities.profileLevels)
106 | info.append("Profile: ")
107 | .append(profile.profile)
108 | .append(", level: ")
109 | .append(profile.level)
110 | .append("\n");
111 | MediaCodecInfo.VideoCapabilities videoCapabilities = null;
112 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
113 | videoCapabilities = codecCapabilities.getVideoCapabilities();
114 |
115 | info.append("Bitrate range: ")
116 | .append(videoCapabilities.getBitrateRange().getLower())
117 | .append(" - ")
118 | .append(videoCapabilities.getBitrateRange().getUpper())
119 | .append("\n")
120 | .append("Frame rate range: ")
121 | .append(videoCapabilities.getSupportedFrameRates().getLower())
122 | .append(" - ")
123 | .append(videoCapabilities.getSupportedFrameRates().getUpper())
124 | .append("\n")
125 | .append("Width range: ")
126 | .append(videoCapabilities.getSupportedWidths().getLower())
127 | .append(" - ")
128 | .append(videoCapabilities.getSupportedWidths().getUpper())
129 | .append("\n")
130 | .append("Height range: ")
131 | .append(videoCapabilities.getSupportedHeights().getLower())
132 | .append(" - ")
133 | .append(videoCapabilities.getSupportedHeights().getUpper())
134 | .append("\n");
135 | }
136 | info.append("----- -----\n");
137 | } else {
138 | info.append("----- Audio info -----\n");
139 | for (MediaCodecInfo.CodecProfileLevel profile : codecCapabilities.profileLevels)
140 | info.append("Profile: ")
141 | .append(profile.profile)
142 | .append(", level: ")
143 | .append(profile.level)
144 | .append("\n");
145 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
146 | MediaCodecInfo.AudioCapabilities audioCapabilities =
147 | codecCapabilities.getAudioCapabilities();
148 |
149 | info.append("Bitrate range: ")
150 | .append(audioCapabilities.getBitrateRange().getLower())
151 | .append(" - ")
152 | .append(audioCapabilities.getBitrateRange().getUpper())
153 | .append("\n")
154 | .append("Channels supported: ")
155 | .append(audioCapabilities.getMaxInputChannelCount())
156 | .append("\n");
157 | try {
158 | if (audioCapabilities.getSupportedSampleRates() != null
159 | && audioCapabilities.getSupportedSampleRates().length > 0) {
160 | info.append("Supported sample rate: \n");
161 | for (int sr : audioCapabilities.getSupportedSampleRates())
162 | info.append(sr)
163 | .append("\n");
164 | }
165 | } catch (Exception ignored) {
166 | }
167 | }
168 | info.append("----- -----\n");
169 | }
170 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
171 | info.append("Max instances: ")
172 | .append(codecCapabilities.getMaxSupportedInstances())
173 | .append("\n");
174 | }
175 | }
176 | info.append("----------------\n");
177 | infos.add(info.toString());
178 | }
179 | return infos;
180 | }
181 |
182 | public static List getAllCodecs(boolean filterBroken) {
183 | List mediaCodecInfoList = new ArrayList<>();
184 | if (Build.VERSION.SDK_INT >= 21) {
185 | MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
186 | MediaCodecInfo[] mediaCodecInfos = mediaCodecList.getCodecInfos();
187 | mediaCodecInfoList.addAll(Arrays.asList(mediaCodecInfos));
188 | } else {
189 | int count = MediaCodecList.getCodecCount();
190 | for (int i = 0; i < count; i++) {
191 | MediaCodecInfo mci = MediaCodecList.getCodecInfoAt(i);
192 | mediaCodecInfoList.add(mci);
193 | }
194 | }
195 | return filterBroken ? filterBrokenCodecs(mediaCodecInfoList) : mediaCodecInfoList;
196 | }
197 |
198 | public static List getAllHardwareEncoders(String mime, boolean cbrPriority) {
199 | List mediaCodecInfoList = getAllEncoders(mime);
200 | List mediaCodecInfoHardware = new ArrayList<>();
201 | List mediaCodecInfoHardwareCBR = new ArrayList<>();
202 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
203 | if (isHardwareAccelerated(mediaCodecInfo)) {
204 | mediaCodecInfoHardware.add(mediaCodecInfo);
205 | if (cbrPriority && isCBRModeSupported(mediaCodecInfo, mime)) {
206 | mediaCodecInfoHardwareCBR.add(mediaCodecInfo);
207 | }
208 | }
209 | }
210 | mediaCodecInfoHardware.removeAll(mediaCodecInfoHardwareCBR);
211 | mediaCodecInfoHardware.addAll(0, mediaCodecInfoHardwareCBR);
212 | return mediaCodecInfoHardware;
213 | }
214 |
215 | public static List getAllHardwareEncoders(String mime) {
216 | return getAllHardwareEncoders(mime, false);
217 | }
218 |
219 |
220 | public static List getAllHardwareDecoders(String mime) {
221 | List mediaCodecInfoList = getAllDecoders(mime);
222 | List mediaCodecInfoHardware = new ArrayList<>();
223 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
224 | if (isHardwareAccelerated(mediaCodecInfo)) {
225 | mediaCodecInfoHardware.add(mediaCodecInfo);
226 | }
227 | }
228 | return mediaCodecInfoHardware;
229 | }
230 |
231 | public static List getAllSoftwareEncoders(String mime, boolean cbrPriority) {
232 | List mediaCodecInfoList = getAllEncoders(mime);
233 | List mediaCodecInfoSoftware = new ArrayList<>();
234 | List mediaCodecInfoSoftwareCBR = new ArrayList<>();
235 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
236 | if (isSoftwareOnly(mediaCodecInfo)) {
237 | mediaCodecInfoSoftware.add(mediaCodecInfo);
238 | if (cbrPriority && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
239 | && isCBRModeSupported(mediaCodecInfo, mime)) {
240 | mediaCodecInfoSoftwareCBR.add(mediaCodecInfo);
241 | }
242 | }
243 | }
244 | mediaCodecInfoSoftware.removeAll(mediaCodecInfoSoftwareCBR);
245 | mediaCodecInfoSoftware.addAll(0, mediaCodecInfoSoftwareCBR);
246 | return mediaCodecInfoSoftware;
247 | }
248 |
249 | public static List getAllSoftwareEncoders(String mime) {
250 | return getAllSoftwareEncoders(mime, false);
251 | }
252 |
253 |
254 | public static List getAllSoftwareDecoders(String mime) {
255 | List mediaCodecInfoList = getAllDecoders(mime);
256 | List mediaCodecInfoSoftware = new ArrayList<>();
257 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
258 | if (isSoftwareOnly(mediaCodecInfo)) {
259 | mediaCodecInfoSoftware.add(mediaCodecInfo);
260 | }
261 | }
262 | return mediaCodecInfoSoftware;
263 | }
264 |
265 | /**
266 | * choose encoder by mime.
267 | */
268 | public static List getAllEncoders(String mime) {
269 | List mediaCodecInfoList = new ArrayList<>();
270 | List mediaCodecInfos = getAllCodecs(true);
271 | for (MediaCodecInfo mci : mediaCodecInfos) {
272 | if (!mci.isEncoder()) {
273 | continue;
274 | }
275 | String[] types = mci.getSupportedTypes();
276 | for (String type : types) {
277 | if (type.equalsIgnoreCase(mime)) {
278 | mediaCodecInfoList.add(mci);
279 | }
280 | }
281 | }
282 | return mediaCodecInfoList;
283 | }
284 |
285 | public static List getAllEncoders(String mime, boolean hardwarePriority, boolean cbrPriority) {
286 | List mediaCodecInfoList = new ArrayList<>();
287 | if (hardwarePriority) {
288 | mediaCodecInfoList.addAll(getAllHardwareEncoders(mime, cbrPriority));
289 | mediaCodecInfoList.addAll(getAllSoftwareEncoders(mime, cbrPriority));
290 | } else {
291 | mediaCodecInfoList.addAll(getAllEncoders(mime));
292 | }
293 | return mediaCodecInfoList;
294 | }
295 |
296 | public static List getAllEncoders(String mime, boolean hardwarePriority) {
297 | return getAllEncoders(mime, hardwarePriority, false);
298 | }
299 |
300 |
301 | /**
302 | * choose decoder by mime.
303 | */
304 | public static List getAllDecoders(String mime) {
305 | List mediaCodecInfoList = new ArrayList<>();
306 | List mediaCodecInfos = getAllCodecs(true);
307 | for (MediaCodecInfo mci : mediaCodecInfos) {
308 | if (mci.isEncoder()) {
309 | continue;
310 | }
311 | String[] types = mci.getSupportedTypes();
312 | for (String type : types) {
313 | if (type.equalsIgnoreCase(mime)) {
314 | mediaCodecInfoList.add(mci);
315 | }
316 | }
317 | }
318 | return mediaCodecInfoList;
319 | }
320 |
321 | public static List getAllDecoders(String mime, boolean hardwarePriority) {
322 | List mediaCodecInfoList = new ArrayList<>();
323 | if (hardwarePriority) {
324 | mediaCodecInfoList.addAll(getAllHardwareDecoders(mime));
325 | mediaCodecInfoList.addAll(getAllSoftwareDecoders(mime));
326 | } else {
327 | mediaCodecInfoList.addAll(getAllDecoders(mime));
328 | }
329 | return mediaCodecInfoList;
330 | }
331 |
332 | /* Adapted from google/ExoPlayer
333 | * https://github.com/google/ExoPlayer/commit/48555550d7fcf6953f2382466818c74092b26355
334 | */
335 | private static boolean isHardwareAccelerated(MediaCodecInfo codecInfo) {
336 | if (Build.VERSION.SDK_INT >= 29) {
337 | return codecInfo.isHardwareAccelerated();
338 | }
339 | // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true.
340 | // However, we assume this to be true as an approximation.
341 | return !isSoftwareOnly(codecInfo);
342 | }
343 |
344 | /* Adapted from google/ExoPlayer
345 | * https://github.com/google/ExoPlayer/commit/48555550d7fcf6953f2382466818c74092b26355
346 | */
347 | private static boolean isSoftwareOnly(MediaCodecInfo mediaCodecInfo) {
348 | if (Build.VERSION.SDK_INT >= 29) {
349 | //mediaCodecInfo.isSoftwareOnly() is not working on emulators.
350 | //Use !mediaCodecInfo.isHardwareAccelerated() to make sure that all codecs are classified as software or hardware
351 | return !mediaCodecInfo.isHardwareAccelerated();
352 | }
353 | String name = mediaCodecInfo.getName().toLowerCase();
354 | if (name.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs
355 | return false;
356 | }
357 | return name.startsWith("omx.google.")
358 | || name.startsWith("omx.ffmpeg.")
359 | || (name.startsWith("omx.sec.") && name.contains(".sw."))
360 | || name.equals("omx.qcom.video.decoder.hevcswvdec")
361 | || name.startsWith("c2.android.")
362 | || name.startsWith("c2.google.")
363 | || (!name.startsWith("omx.") && !name.startsWith("c2."));
364 | }
365 |
366 | //@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
367 | public static boolean isCBRModeSupported(MediaCodecInfo mediaCodecInfo, String mime) {
368 | MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mime);
369 | MediaCodecInfo.EncoderCapabilities encoderCapabilities = codecCapabilities.getEncoderCapabilities();
370 | return encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
371 | }
372 |
373 | public static boolean isVBRModeSupported(MediaCodecInfo mediaCodecInfo, String mime) {
374 | MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mime);
375 | MediaCodecInfo.EncoderCapabilities encoderCapabilities = codecCapabilities.getEncoderCapabilities();
376 | return encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
377 | }
378 |
379 | /**
380 | * Filter broken codecs by name and device model.
381 | *
382 | * Note:
383 | * There is no way to know broken encoders so we will check by name and device.
384 | * Please add your encoder to this method if you detect one.
385 | *
386 | * @param codecs All device codecs
387 | * @return a list without broken codecs
388 | */
389 | private static List filterBrokenCodecs(List codecs) {
390 | List listFilter = new ArrayList<>();
391 | List listLowPriority = new ArrayList<>();
392 | List listUltraLowPriority = new ArrayList<>();
393 | for (MediaCodecInfo mediaCodecInfo : codecs) {
394 | if (isValid(mediaCodecInfo.getName())) {
395 | CodecPriority priority = checkCodecPriority(mediaCodecInfo.getName());
396 | switch (priority) {
397 | case ULTRA_LOW:
398 | listUltraLowPriority.add(mediaCodecInfo);
399 | break;
400 | case LOW:
401 | listLowPriority.add(mediaCodecInfo);
402 | break;
403 | case NORMAL:
404 | default:
405 | listFilter.add(mediaCodecInfo);
406 | break;
407 | }
408 | }
409 | }
410 | listFilter.addAll(listLowPriority);
411 | listFilter.addAll(listUltraLowPriority);
412 | return listFilter;
413 | }
414 |
415 | /**
416 | * For now, none broken codec reported.
417 | */
418 | private static boolean isValid(String name) {
419 | //This encoder is invalid and produce errors (Only found in AVD API 16)
420 | if (name.equalsIgnoreCase("aacencoder")) return false;
421 | return true;
422 | }
423 |
424 | private enum CodecPriority {
425 | NORMAL, LOW, ULTRA_LOW
426 | }
427 |
428 | /**
429 | * Few devices have codecs that is not working properly in few cases like using AWS MediaLive or YouTube
430 | * but it is still usable in most of cases.
431 | *
432 | * @return priority level.
433 | */
434 | private static CodecPriority checkCodecPriority(String name) {
435 | //maybe only broke on samsung with Android 12+ using YouTube and AWS MediaLive
436 | // but set as ultra low priority in all cases.
437 | if (name.equalsIgnoreCase("c2.sec.aac.encoder")) return CodecPriority.ULTRA_LOW;
438 | //broke on few devices using YouTube and AWS MediaLive
439 | else if (name.equalsIgnoreCase("omx.google.aac.encoder")) return CodecPriority.LOW;
440 | else return CodecPriority.NORMAL;
441 | }
442 | }
443 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/ColorFormatUtil.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.utils
2 |
3 | /**
4 | * @author Key
5 | * Time: 2022/10/11 16:27
6 | * Description:
7 | */
8 | object ColorFormatUtil {
9 | fun encodeYUV420SP(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int) {
10 | val frameSize = width * height
11 | var yIndex = 0
12 | var uvIndex = frameSize
13 | var index = 0
14 | for (j in 0 until height) {
15 | for (i in 0 until width) {
16 | // val a = argb[index] and -0x1000000 shr 24
17 | val r = argb[index] and 0xff0000 shr 16
18 | val g = argb[index] and 0xff00 shr 8
19 | val b = argb[index] and 0xff shr 0
20 | val y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
21 | val u = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
22 | val v = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
23 | yuv420sp[yIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
24 | if (j % 2 == 0 && index % 2 == 0) {
25 | yuv420sp[uvIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
26 | yuv420sp[uvIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
27 | }
28 | index++
29 | }
30 | }
31 | }
32 |
33 | fun encodeYUV420P(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int) {
34 | val frameSize = width * height
35 | var yIndex = 0
36 | var uIndex = frameSize
37 | var vIndex = frameSize + width * height / 4
38 | var index = 0
39 | for (j in 0 until height) {
40 | for (i in 0 until width) {
41 | // val a = argb[index] and -0x1000000 shr 24
42 | val r = argb[index] and 0xff0000 shr 16
43 | val g = argb[index] and 0xff00 shr 8
44 | val b = argb[index] and 0xff shr 0
45 | val y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
46 | val u = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
47 | val v = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
48 | yuv420sp[yIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
49 | if (j % 2 == 0 && index % 2 == 0) {
50 | yuv420sp[vIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
51 | yuv420sp[uIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
52 | }
53 | index++
54 | }
55 | }
56 | }
57 |
58 | fun encodeYUV420PSP(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int) {
59 | var yIndex = 0
60 | var index = 0
61 | for (j in 0 until height) {
62 | for (i in 0 until width) {
63 | // val a = argb[index] and -0x1000000 shr 24
64 | val r = argb[index] and 0xff0000 shr 16
65 | val g = argb[index] and 0xff00 shr 8
66 | val b = argb[index] and 0xff shr 0
67 | val y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
68 | val u = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
69 | val v = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
70 | yuv420sp[yIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
71 | if (j % 2 == 0 && index % 2 == 0) {
72 | yuv420sp[yIndex + 1] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
73 | yuv420sp[yIndex + 3] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
74 | }
75 | if (index % 2 == 0) {
76 | yIndex++
77 | }
78 | index++
79 | }
80 | }
81 | }
82 |
83 | fun encodeYUV420PP(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int) {
84 | var yIndex = 0
85 | var vIndex = yuv420sp.size / 2
86 | var index = 0
87 | for (j in 0 until height) {
88 | for (i in 0 until width) {
89 | // val a = argb[index] and -0x1000000 shr 24
90 | val r = argb[index] and 0xff0000 shr 16
91 | val g = argb[index] and 0xff00 shr 8
92 | val b = argb[index] and 0xff shr 0
93 | val y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
94 | val u = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
95 | val v = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
96 | if (j % 2 == 0 && index % 2 == 0) { // 0
97 | yuv420sp[yIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
98 | yuv420sp[yIndex + 1] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
99 | yuv420sp[vIndex + 1] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
100 | yIndex++
101 | } else if (j % 2 == 0 && index % 2 == 1) { //1
102 | yuv420sp[yIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
103 | } else if (j % 2 == 1 && index % 2 == 0) { //2
104 | yuv420sp[vIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
105 | vIndex++
106 | } else if (j % 2 == 1 && index % 2 == 1) { //3
107 | yuv420sp[vIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
108 | }
109 | index++
110 | }
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/EncoderTools.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.utils
2 |
3 | import android.graphics.Bitmap
4 | import android.media.MediaCodecInfo
5 | import android.media.MediaCodecList
6 |
7 | /**
8 | * @author Key
9 | * Time: 2022/10/11 16:27
10 | * Description:
11 | */
12 | object EncoderTools {
13 | /**
14 | * 从我打印的capabilitiesForType.colorFormats来看,确实全部都支持:2135033992,另外出现的较多的是COLOR_FormatSurface
15 | * 从测试结果看 全部采用COLOR_FormatYUV420Flexible在某些机型上会导致花屏
16 | * private var mColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
17 | */
18 | fun getColorFormat(): Int {
19 | var colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
20 | val formats = mediaCodecList()
21 | lab@ for (format in formats) {
22 | when (format) {
23 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar -> {
24 | colorFormat = format
25 | break@lab
26 | }
27 |
28 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar -> {
29 | colorFormat = format
30 | break@lab
31 | }
32 |
33 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar -> {
34 | colorFormat = format
35 | break@lab
36 | }
37 |
38 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar -> {
39 | colorFormat = format
40 | break@lab
41 | }
42 |
43 | else -> break@lab
44 | }
45 | }
46 | return colorFormat
47 | }
48 |
49 | fun getPixels(colorFormat: Int, inputWidth: Int, inputHeight: Int, scaled: Bitmap): ByteArray {
50 | val argb = IntArray(inputWidth * inputHeight)
51 | scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight)
52 | val yuv = ByteArray(inputWidth * inputHeight * 3 / 2)
53 | when (colorFormat) {
54 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar -> ColorFormatUtil.encodeYUV420SP(
55 | yuv,
56 | argb,
57 | inputWidth,
58 | inputHeight
59 | )
60 |
61 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar -> ColorFormatUtil.encodeYUV420P(
62 | yuv,
63 | argb,
64 | inputWidth,
65 | inputHeight
66 | )
67 |
68 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar -> ColorFormatUtil.encodeYUV420PSP(
69 | yuv,
70 | argb,
71 | inputWidth,
72 | inputHeight
73 | )
74 |
75 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar -> ColorFormatUtil.encodeYUV420PP(
76 | yuv,
77 | argb,
78 | inputWidth,
79 | inputHeight
80 | )
81 | }
82 | return yuv
83 | }
84 |
85 | /**
86 | * 获取可以支持的格式
87 | */
88 | private fun mediaCodecList(): IntArray {
89 | val numCodecs = MediaCodecList.getCodecCount()
90 | var codecInfo: MediaCodecInfo? = null
91 | var i = 0
92 | while (i < numCodecs && codecInfo == null) {
93 | val info = MediaCodecList.getCodecInfoAt(i)
94 | if (!info.isEncoder) {
95 | i++
96 | continue
97 | }
98 | val types = info.supportedTypes
99 | var found = false
100 | // The decoder required by the rotation training
101 | var j = 0
102 | while (j < types.size && !found) {
103 | if (types[j] == "video/avc") {
104 | found = true
105 | }
106 | j++
107 | }
108 | if (!found) {
109 | i++
110 | continue
111 | }
112 | codecInfo = info
113 | i++
114 | }
115 | val capabilities = codecInfo!!.getCapabilitiesForType("video/avc")
116 | return capabilities.colorFormats
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/FrameUtil.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.utils
2 |
3 | import java.nio.ByteBuffer
4 |
5 | /**
6 | * Description:
7 | *
8 | * Time: 2023/11/21 21:17
9 | * @author Key
10 | */
11 | object FrameUtil {
12 | //H264 IDR
13 | const val IDR = 5
14 |
15 | //H265 IDR
16 | const val IDR_N_LP = 20
17 | const val IDR_W_DLP = 19
18 |
19 | fun isKeyFrame(videoMime: String, videoBuffer: ByteBuffer): Boolean {
20 | val header = ByteArray(5)
21 | videoBuffer.duplicate()[header, 0, header.size]
22 | return if (videoMime == CodecUtil.H264_MIME && header[4].toInt() and 0x1F == IDR) { //h264
23 | true
24 | } else { //h265
25 | (videoMime == CodecUtil.H265_MIME && header[4].toInt() shr 1 and 0x3f == IDR_W_DLP
26 | || header[4].toInt() shr 1 and 0x3f == IDR_N_LP)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/RecordViewUtil.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.utils
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.Canvas
5 | import android.graphics.Rect
6 | import android.os.Build
7 | import android.os.Handler
8 | import android.os.Looper
9 | import android.util.Log
10 | import android.view.PixelCopy
11 | import android.view.View
12 | import android.view.Window
13 | import androidx.annotation.RequiresApi
14 | import java.util.concurrent.CompletableFuture
15 | import java.util.concurrent.TimeUnit
16 |
17 |
18 | /**
19 | * @author Key
20 | * Time: 2022/09/01 14:46
21 | * Description: Bitmap.Config.RGB_565 在某些手机上(已知三星S9)不设置跟布局背景色会是透明的,用565录制会变成黑色
22 | */
23 | object RecordViewUtil {
24 | private const val TAG = "RecordViewUtil"
25 | private val mBitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888
26 | private val mMainHandler = Handler(Looper.getMainLooper())
27 |
28 | /**
29 | * @param width 指定宽度,等比例缩放高度
30 | */
31 | @Throws
32 | fun getBitmapFromView(window: Window, targetView: View, width: Int? = null): Bitmap {
33 | val finalWidth = width ?: targetView.width
34 | if (finalWidth <= 0) {
35 | //Log.w(TAG, "finalWidth=$finalWidth")
36 | throw IllegalArgumentException("宽度小于等于0, finalWidth=$finalWidth")
37 | }
38 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
39 | copyPixelFromView(window, targetView, finalWidth)
40 | } else {
41 | convertViewToBitmap(targetView, finalWidth)
42 | }
43 | }
44 |
45 | /**
46 | * Android 26(O)(8.0)以下的版本,使用此方法,某些情况下颜色有偏差,已经View采集不全,比如Android14上摄像头内容未采集到
47 | * 如果采用drawingCache.copy宽高未生效,还是View原始的宽高,要生效需采用Canvas方式
48 | * drawingCache.copy耗时:6-9ms, Canvas方式还更快,所以目前改为Canvas实现
49 | */
50 | @Throws
51 | fun convertViewToBitmap(targetView: View, width: Int): Bitmap {
52 | // 优化宽高
53 | val recordWidth = if (width % 2 != 0) {
54 | width - 1
55 | } else {
56 | width
57 | }
58 | var recordHeight = if (recordWidth == targetView.width) {
59 | // 宽度不变,则高度也不变
60 | targetView.height
61 | } else {
62 | (targetView.height * (recordWidth.toFloat() / targetView.width)).toInt()
63 | }
64 | if (recordHeight % 2 != 0) {
65 | recordHeight -= 1
66 | }
67 | val bitmap = Bitmap.createBitmap(recordWidth, recordHeight, mBitmapConfig)
68 | val canvas = Canvas(bitmap)
69 | // 保存当前状态,目前只改变一次,多余
70 | //val saveCount = canvas.save()
71 | // 缩放Canvas来匹配目标Bitmap
72 | canvas.scale(recordWidth.toFloat() / targetView.width, recordHeight.toFloat() / targetView.height)
73 | // 将View绘制到Canvas上
74 | targetView.draw(canvas)
75 | // 恢复Canvas状态
76 | //canvas.restoreToCount(saveCount)
77 | return bitmap
78 | }
79 |
80 | /**
81 | * @param width 指定宽度,等比例缩放高度
82 | * @param targetView 只是为了算坐标,没其他用
83 | */
84 | @RequiresApi(Build.VERSION_CODES.O)
85 | @Throws
86 | fun copyPixelFromView(window: Window, targetView: View, width: Int): Bitmap {
87 | //Log.i(TAG, "current Thread: ${Thread.currentThread().name}")
88 | // 优化宽高
89 | val recordWidth = if (width % 2 != 0) {
90 | width - 1
91 | } else {
92 | width
93 | }
94 | var recordHeight = if (recordWidth == targetView.width) {
95 | // 宽度不变,则高度也不变
96 | targetView.height
97 | } else {
98 | (targetView.height * (recordWidth.toFloat() / targetView.width)).toInt()
99 | }
100 | if (recordHeight % 2 != 0) {
101 | recordHeight -= 1
102 | }
103 |
104 | // 黑屏
105 | // val bitmap = Bitmap.createBitmap(recordWidth, recordHeight, Bitmap.Config.RGB_565)
106 | //准备一个bitmap对象,用来将copy出来的区域绘制到此对象中,view应该是没有alpha的
107 | val bitmap = Bitmap.createBitmap(recordWidth, recordHeight, mBitmapConfig)
108 |
109 | //获取view在Window中的left-top顶点位置,基本上取的当前的window,且录的都是全部,所以都是[0,0]
110 | val location = IntArray(2)
111 | targetView.getLocationInWindow(location)
112 | var isSuccessful = false
113 | //请求转换
114 | //val start = System.currentTimeMillis()
115 | if (!window.isActive) {
116 | textErrorBitmap(bitmap, "窗口未激活")
117 | return bitmap
118 | }
119 | //val start = System.currentTimeMillis()
120 | //val latch = CountDownLatch(1)
121 | val future = CompletableFuture()
122 | try {
123 | PixelCopy.request(
124 | window,
125 | // 截图区域的取值,左上右下
126 | Rect(
127 | location[0],
128 | location[1],
129 | location[0] + targetView.width,
130 | location[1] + targetView.height
131 | ),
132 | bitmap,
133 | { copyResult ->
134 | // 走完外面才有回调,问题不大,当然最稳妥是回调,但是回调不能同步,得加协程
135 | //Log.i(TAG, "回调内isSuccessful=${copyResult == PixelCopy.SUCCESS}, 耗时=${System.currentTimeMillis() - start}ms")
136 | //isSuccessful = copyResult == PixelCopy.SUCCESS
137 | //latch.countDown()
138 | future.complete(copyResult == PixelCopy.SUCCESS)
139 | },
140 | mMainHandler
141 | )
142 | //latch.await(100, TimeUnit.MILLISECONDS)
143 | isSuccessful = future.get(100, TimeUnit.MILLISECONDS)
144 | } catch (e: Exception) {
145 | e.printStackTrace()
146 | textErrorBitmap(bitmap, Log.getStackTraceString(e))
147 | }
148 | //Log.i(TAG, "回调外isSuccessful=$isSuccessful, 耗时=${System.currentTimeMillis() - start}ms")
149 | if (!isSuccessful) {
150 | textErrorBitmap(bitmap)
151 | }
152 | return bitmap
153 | }
154 |
155 | private fun textErrorBitmap(bitmap: Bitmap, message: String? = "图像丢失") {
156 | // 在bitmap上写上错误信息
157 | //val start = System.currentTimeMillis()
158 | val canvas = Canvas(bitmap)
159 | val paint = android.graphics.Paint()
160 | paint.color = android.graphics.Color.RED
161 | paint.textSize = 50f
162 | canvas.drawText(message.takeIf { !it.isNullOrBlank() } ?: "图像丢失!", 10f, 100f, paint)
163 | //Log.i(TAG, "textErrorBitmap耗时=${System.currentTimeMillis() - start}ms")
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/VRLogger.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.utils
2 |
3 | import android.util.Log
4 |
5 | object VRLogger {
6 | private const val TAG = "ViewRecord"
7 | var logLevel = Log.WARN
8 |
9 | @JvmStatic
10 | fun v(msg: String) {
11 | if (logLevel <= Log.VERBOSE) {
12 | Log.v(TAG, getThreadName() + msg)
13 | }
14 | }
15 |
16 | @JvmStatic
17 | fun d(msg: String) {
18 | if (logLevel <= Log.DEBUG) {
19 | Log.d(TAG, getThreadName() + msg)
20 | }
21 | }
22 |
23 | @JvmStatic
24 | fun i(msg: String) {
25 | if (logLevel <= Log.INFO) {
26 | Log.i(TAG, getThreadName() + msg)
27 | }
28 | }
29 |
30 | @JvmStatic
31 | fun w(msg: String, tr: Throwable? = null) {
32 | if (logLevel <= Log.WARN) {
33 | Log.w(TAG, getThreadName() + msg, tr)
34 | }
35 | }
36 |
37 | @JvmStatic
38 | fun e(msg: String, tr: Throwable? = null) {
39 | if (logLevel <= Log.ERROR) {
40 | Log.e(TAG, getThreadName() + msg, tr)
41 | }
42 | }
43 |
44 | private fun getThreadName(): String {
45 | return "Thread: ${Thread.currentThread().name} - "
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/utils/yuv/ConvertUtil.java:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.utils.yuv;
2 |
3 | import android.graphics.Bitmap;
4 | import android.media.MediaCodecInfo;
5 |
6 | /**
7 | * Description: argb的提取和YUV的转换都是一样的,不一样的只是放到的位置不同
8 | *
9 | * YUV420 Semi-Planar (NV12/NV21(Packed)): 效率:高,因为内存布局紧凑,UV 分量存储在一起,减少了内存访问。
10 | * YUV420 Planar (I420): 效率:次高,因为虽然每个分量单独存储,但 Y、U、V 分量可以独立访问,减少了数据交错的复杂度。
11 | *
12 | * Example YUV images 4x4 px.
13 | *
14 | * semi planar NV12 example:
15 | *
16 | * Y1 Y2 Y3 Y4
17 | * Y5 Y6 Y7 Y8
18 | * Y9 Y10 Y11 Y12
19 | * Y13 Y14 Y15 Y16
20 | * V1 U1 V2 U2
21 | * V3 U3 V4 U4
22 | *
23 | *
24 | * Packed Semi Planar NV21 example:
25 | *
26 | * Y1 Y2 Y3 Y4
27 | * Y5 Y6 Y7 Y8
28 | * Y9 Y10 Y11 Y12
29 | * Y13 Y14 Y15 Y16
30 | * U1 V1 U2 V2
31 | * U3 V3 U4 V4
32 | *
33 | *
34 | * Packed Planar YV12 example:
35 | *
36 | * Y1 Y2 Y3 Y4
37 | * Y5 Y6 Y7 Y8
38 | * Y9 Y10 Y11 Y12
39 | * Y13 Y14 Y15 Y16
40 | * U1 U2 U3 U4
41 | * V1 V2 V3 V4
42 | *
43 | *
44 | * planar YV21(I420) example:
45 | *
46 | * Y1 Y2 Y3 Y4
47 | * Y5 Y6 Y7 Y8
48 | * Y9 Y10 Y11 Y12
49 | * Y13 Y14 Y15 Y16
50 | * V1 V2 V3 V4
51 | * U1 U2 U3 U4
52 | *
53 | *
54 | * Time: 2024/5/20 18:40
55 | *
56 | * @author Key
57 | */
58 | public class ConvertUtil {
59 | public static byte[] convertBitmapToYUVByteArray(Bitmap bitmap, int colorFormat) {
60 | int width = bitmap.getWidth();
61 | int height = bitmap.getHeight();
62 | int[] argb = new int[width * height];
63 | bitmap.getPixels(argb, 0, width, 0, 0, width, height);
64 | return switch (colorFormat) {
65 | case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar ->
66 | convertToYUV420SemiPlanar(argb, width, height);
67 | case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar ->
68 | convertToYUV420Planar(argb, width, height);
69 | case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar ->
70 | convertToYUV420PackedPlanar(argb, width, height);
71 | case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar ->
72 | convertToYUV420PackedSemiPlanar(argb, width, height);
73 | default -> throw new IllegalArgumentException("Unsupported color format: " + colorFormat);
74 | };
75 | }
76 |
77 | /**
78 | * (21)NV12格式,YYYYYYYYY, UV交替存储(UVUVUV...)
79 | */
80 | private static byte[] convertToYUV420SemiPlanar(int[] argb, int width, int height) {
81 | byte[] yuv = new byte[width * height * 3 / 2];
82 | int frameSize = width * height;
83 | int yIndex = 0;
84 | int uvIndex = frameSize;
85 |
86 | for (int j = 0; j < height; j++) {
87 | for (int i = 0; i < width; i++) {
88 | int argbIndex = j * width + i;
89 | int r = (argb[argbIndex] >> 16) & 0xff;
90 | int g = (argb[argbIndex] >> 8) & 0xff;
91 | int b = argb[argbIndex] & 0xff;
92 |
93 | int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
94 | y = Math.max(0, Math.min(255, y));
95 | yuv[yIndex++] = (byte) y;
96 |
97 | if (j % 2 == 0 && i % 2 == 0) {
98 | int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
99 | int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
100 | u = Math.max(0, Math.min(255, u));
101 | v = Math.max(0, Math.min(255, v));
102 |
103 | yuv[uvIndex++] = (byte) u; // NV12: UVUVUV...
104 | yuv[uvIndex++] = (byte) v; // NV12: UVUVUV...
105 | }
106 | }
107 | }
108 | return yuv;
109 | }
110 |
111 | /**
112 | * (39)NV21格式,YYYYYYYYY, VU交替存储(VUVUVU...)
113 | */
114 | private static byte[] convertToYUV420PackedSemiPlanar(int[] argb, int width, int height) {
115 | byte[] yuv = new byte[width * height * 3 / 2];
116 | int frameSize = width * height;
117 | int yIndex = 0;
118 | int uvIndex = frameSize;
119 |
120 | for (int j = 0; j < height; j++) {
121 | for (int i = 0; i < width; i++) {
122 | int argbIndex = j * width + i;
123 | int r = (argb[argbIndex] >> 16) & 0xff;
124 | int g = (argb[argbIndex] >> 8) & 0xff;
125 | int b = argb[argbIndex] & 0xff;
126 |
127 | int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
128 | y = Math.max(0, Math.min(255, y));
129 | yuv[yIndex++] = (byte) y;
130 |
131 | if (j % 2 == 0 && i % 2 == 0) {
132 | int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
133 | u = Math.max(0, Math.min(255, u));
134 | yuv[uvIndex++] = (byte) u;
135 | } else if (j % 2 == 0 && i % 2 == 1) {
136 | int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
137 | v = Math.max(0, Math.min(255, v));
138 | yuv[uvIndex++] = (byte) v;
139 | }
140 | }
141 | }
142 | return yuv;
143 | }
144 |
145 | /**
146 | * (19)YV21格式,YUV分量顺序分开存储: YYYYYYYYYYYYYYYY...UUUU...VVVV...
147 | */
148 | private static byte[] convertToYUV420Planar(int[] argb, int width, int height) {
149 | byte[] yuv = new byte[width * height * 3 / 2];
150 | int frameSize = width * height;
151 | int yIndex = 0;
152 | int uIndex = frameSize;
153 | int vIndex = frameSize + frameSize / 4;
154 |
155 | for (int j = 0; j < height; j++) {
156 | for (int i = 0; i < width; i++) {
157 | int argbIndex = j * width + i;
158 | int r = (argb[argbIndex] >> 16) & 0xff;
159 | int g = (argb[argbIndex] >> 8) & 0xff;
160 | int b = argb[argbIndex] & 0xff;
161 |
162 | int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
163 | y = Math.max(0, Math.min(255, y));
164 | yuv[yIndex++] = (byte) y;
165 |
166 | if (j % 2 == 0 && i % 2 == 0) {
167 | int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
168 | u = Math.max(0, Math.min(255, u));
169 | yuv[uIndex++] = (byte) u;
170 |
171 | int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
172 | v = Math.max(0, Math.min(255, v));
173 | yuv[vIndex++] = (byte) v;
174 | }
175 | }
176 | }
177 | return yuv;
178 | }
179 |
180 | /**
181 | * Packed先V再U,和非Packed相反
182 | * (20)YV12:YYYYYYYYYYYY...VVVVVVVVV...UUUUUUU....
183 | */
184 | private static byte[] convertToYUV420PackedPlanar(int[] argb, int width, int height) {
185 | byte[] yuv = new byte[width * height * 3 / 2];
186 | int frameSize = width * height;
187 | int yIndex = 0;
188 | int uIndex = frameSize;
189 | int vIndex = frameSize + frameSize / 4;
190 |
191 | for (int j = 0; j < height; j++) {
192 | for (int i = 0; i < width; i++) {
193 | int argbIndex = j * width + i;
194 | int r = (argb[argbIndex] >> 16) & 0xff;
195 | int g = (argb[argbIndex] >> 8) & 0xff;
196 | int b = argb[argbIndex] & 0xff;
197 |
198 | int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
199 | y = Math.max(0, Math.min(255, y));
200 | yuv[yIndex++] = (byte) y;
201 |
202 | if (j % 2 == 0 && i % 2 == 0) {
203 | int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
204 | int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
205 | u = Math.max(0, Math.min(255, u));
206 | v = Math.max(0, Math.min(255, v));
207 |
208 | yuv[uIndex++] = (byte) u;
209 | yuv[vIndex++] = (byte) v;
210 | }
211 | }
212 | }
213 | return yuv;
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/EncoderCallback.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.video
17 |
18 | import android.media.MediaCodec
19 | import android.media.MediaFormat
20 |
21 | /**
22 | * Created by pedro on 18/09/19.
23 | */
24 | interface EncoderCallback {
25 | @Throws(IllegalStateException::class)
26 | fun inputAvailable(mediaCodec: MediaCodec, inBufferIndex: Int)
27 |
28 | @Throws(IllegalStateException::class)
29 | fun outputAvailable(mediaCodec: MediaCodec, outBufferIndex: Int, bufferInfo: MediaCodec.BufferInfo)
30 |
31 | fun formatChanged(mediaCodec: MediaCodec, mediaFormat: MediaFormat)
32 | }
33 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/EncoderErrorCallback.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.video
18 |
19 | import android.media.MediaCodec
20 |
21 | /**
22 | * Created by pedro on 18/9/23.
23 | */
24 | interface EncoderErrorCallback {
25 | fun onCodecError(type: String, e: MediaCodec.CodecException)
26 |
27 | /**
28 | * @return indicate if should try reset encoder, 编码过程中 input 和 output 报的错
29 | */
30 | fun onEncodeError(type: String, e: IllegalStateException): Boolean = true
31 | }
32 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/FormatVideoEncoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.video;
18 |
19 | import android.media.MediaCodecInfo;
20 |
21 | import androidx.annotation.NonNull;
22 |
23 | /**
24 | * Created by pedro on 21/01/17.
25 | * NOTE: 目前直接转换只支持21, 19, 39, 20这四种,所以其余的注释掉,如有需要自行查找转换方案,并且在chooseColorDynamically方法中添加转换函数
26 | */
27 |
28 | public enum FormatVideoEncoder {
29 | YUV420_SEMI_PLANAR, YUV420_PLANAR, YUV420_PACKED_PLANAR, YUV420_PACKED_SEMI_PLANAR,
30 | /*
31 | * YUV420Flexible并不是一种确定的YUV420格式,而是包含COLOR_FormatYUV411Planar, COLOR_FormatYUV411PackedPlanar, COLOR_FormatYUV420Planar, COLOR_FormatYUV420PackedPlanar, COLOR_FormatYUV420SemiPlanar和COLOR_FormatYUV420PackedSemiPlanar。
32 | * 在API 21引入YUV420Flexible的同时,它所包含的这些格式都deprecated掉了
33 | * YUV420Flexible是一种灵活的格式,具体使用时依旧需要确定当前使用的子格式才能完成正确的转换,所以放这里没有意义
34 | */
35 | //YUV420_FLEXIBLE,
36 | //YUV422FLEXIBLE, YUV422PLANAR, YUV422SEMIPLANAR, YUV422PACKEDPLANAR, YUV422PACKEDSEMIPLANAR,
37 | //YUV444FLEXIBLE, YUV444INTERLEAVED,
38 | /**
39 | * SURFACE通常用于直接渲染,不需要将 Bitmap 转换为字节数组
40 | */
41 | SURFACE,
42 | /**
43 | * 用于动态获取
44 | */
45 | YUV420Dynamical;
46 |
47 | public int getFormatCodec() {
48 | return switch (this) {
49 | // = NV12 = 21
50 | case YUV420_SEMI_PLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
51 | // = i420 = YV21 =19
52 | case YUV420_PLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
53 | // = NV21 = 39
54 | case YUV420_PACKED_SEMI_PLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar;
55 | // = YV12 = 20
56 | case YUV420_PACKED_PLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar;
57 | //case SURFACE -> MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface;
58 | // 动态格式:2135033992
59 | //case YUV420_FLEXIBLE -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
60 | //case YUV422FLEXIBLE -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV422Flexible;
61 | //case YUV422PLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV422Planar;
62 | //case YUV422SEMIPLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV422SemiPlanar;
63 | //case YUV422PACKEDPLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV422PackedPlanar;
64 | //case YUV422PACKEDSEMIPLANAR -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV422PackedSemiPlanar;
65 | //case YUV444FLEXIBLE -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV444Flexible;
66 | //case YUV444INTERLEAVED -> MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV444Interleaved;
67 | default -> -1;
68 | };
69 | }
70 |
71 | @NonNull
72 | @Override
73 | public String toString() {
74 | return name() + ", int code: " + getFormatCodec();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/FpsLimiter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.video;
18 |
19 |
20 | /**
21 | * 2024/04/04 修改
22 | */
23 | public class FpsLimiter {
24 | private long lastFrameTime = System.currentTimeMillis();
25 | private double ratioF = 1000.0 / 30;
26 |
27 | public void setFPS(int fps) {
28 | setCurrentFrameTime();
29 | ratioF = 1000.0 / fps;
30 | }
31 |
32 | /**
33 | * 大于0表示需要等待,小于等于0表示不需要等待
34 | *
35 | * @return 返回需要等待的时间
36 | */
37 | public long limitFPS() {
38 | // 距离上一帧时间
39 | long sinceLastFrameTime = System.currentTimeMillis() - lastFrameTime;
40 | return (long) (ratioF - sinceLastFrameTime);
41 | }
42 |
43 | public void setCurrentFrameTime() {
44 | setLastFrameTime(System.currentTimeMillis());
45 | }
46 |
47 | public void setLastFrameTime(long lastFrameTime) {
48 | this.lastFrameTime = lastFrameTime;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/GetVideoData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.keyss.view_record.video
17 |
18 | import android.media.MediaCodec
19 | import android.media.MediaFormat
20 | import java.nio.ByteBuffer
21 |
22 | /**
23 | * Created by pedro on 20/01/17.
24 | */
25 | interface GetVideoData {
26 | fun getVideoData(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo)
27 | fun onVideoFormat(mediaFormat: MediaFormat)
28 | }
29 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/IFrameDataGetter.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record.video
2 |
3 | import io.keyss.view_record.base.Frame
4 |
5 | /**
6 | * Description:
7 | *
8 | * Time: 2023/11/22 14:34
9 | * @author Key
10 | */
11 | interface IFrameDataGetter {
12 | fun getFrameData(): Frame
13 | }
14 |
--------------------------------------------------------------------------------
/lib-view-record/src/main/java/io/keyss/view_record/video/VideoEncoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 pedroSG94.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.keyss.view_record.video;
18 |
19 | import android.media.MediaCodec;
20 | import android.media.MediaCodecInfo;
21 | import android.media.MediaFormat;
22 | import android.os.Bundle;
23 | import android.os.SystemClock;
24 | import android.util.Log;
25 | import android.view.Surface;
26 |
27 | import androidx.annotation.NonNull;
28 |
29 | import java.nio.ByteBuffer;
30 | import java.util.Arrays;
31 | import java.util.List;
32 |
33 | import io.keyss.view_record.base.BaseEncoder;
34 | import io.keyss.view_record.base.Frame;
35 | import io.keyss.view_record.utils.CodecUtil;
36 |
37 | /**
38 | * Created by pedro on 19/01/17.
39 | * This class need use same resolution, fps and imageFormat that Camera1ApiManagerGl
40 | */
41 |
42 | public class VideoEncoder extends BaseEncoder {
43 | private final GetVideoData getVideoData;
44 | //surface to buffer encoder
45 | private Surface inputSurface;
46 | private int width = 640;
47 | private int height = 480;
48 | private int fps = 24;
49 | private int bitRate = 1280 * 1024; //in kbps
50 | // I帧间隔:秒
51 | private int iFrameInterval = 1;
52 | //for disable video
53 | private final FpsLimiter fpsLimiter = new FpsLimiter();
54 | private String type = CodecUtil.H264_MIME;
55 | private FormatVideoEncoder formatVideoEncoder = FormatVideoEncoder.YUV420Dynamical;
56 | private int avcProfile = -1;
57 | private int avcProfileLevel = -1;
58 |
59 | public VideoEncoder(GetVideoData getVideoData) {
60 | this.getVideoData = getVideoData;
61 | TAG = "VideoEncoder";
62 | }
63 |
64 | public boolean prepareVideoEncoder(int width, int height, int fps, int bitRate, int iFrameInterval, FormatVideoEncoder formatVideoEncoder) {
65 | return prepareVideoEncoder(width, height, fps, bitRate, iFrameInterval, formatVideoEncoder, -1, -1);
66 | }
67 |
68 | /**
69 | * Prepare encoder with custom parameters
70 | */
71 | public boolean prepareVideoEncoder(int width, int height, int fps, int bitRate,
72 | int iFrameInterval, FormatVideoEncoder formatVideoEncoder,
73 | int avcProfile, int avcProfileLevel) {
74 | this.width = width;
75 | this.height = height;
76 | this.fps = fps;
77 | this.bitRate = bitRate;
78 | this.iFrameInterval = iFrameInterval;
79 | this.formatVideoEncoder = formatVideoEncoder;
80 | this.avcProfile = avcProfile;
81 | this.avcProfileLevel = avcProfileLevel;
82 | MediaCodecInfo encoder = chooseEncoder(type);
83 | try {
84 | if (encoder != null) {
85 | // 返回不为null,说明设置的颜色和格式都支持
86 | Log.i(TAG, "Video Encoder selected " + encoder.getName());
87 | codec = MediaCodec.createByCodecName(encoder.getName());
88 | if (this.formatVideoEncoder == FormatVideoEncoder.YUV420Dynamical) {
89 | this.formatVideoEncoder = chooseColorDynamically(encoder);
90 | if (this.formatVideoEncoder == null) {
91 | Log.e(TAG, "YUV420 dynamical choose failed");
92 | return false;
93 | }
94 | } else {
95 | // 验证选择的颜色是否在颜色支持列表
96 | //Arrays.stream(encoder.getCapabilitiesForType(type).colorFormats).anyMatch(element -> element == formatVideoEncoder.getFormatCodec());
97 | boolean contains = false;
98 | for (int colorFormat : encoder.getCapabilitiesForType(type).colorFormats) {
99 | if (colorFormat == formatVideoEncoder.getFormatCodec()) {
100 | contains = true;
101 | break;
102 | }
103 | }
104 | if (!contains) {
105 | Log.e(TAG, "非动态, 手动选择的Color format [" + formatVideoEncoder + "] not supported");
106 | return false;
107 | }
108 | }
109 | Log.i(TAG, "YUV420 choose: " + this.formatVideoEncoder.toString() + ", 传入: " + formatVideoEncoder);
110 | } else {
111 | Log.e(TAG, "Valid encoder not found");
112 | return false;
113 | }
114 | MediaFormat videoFormat = MediaFormat.createVideoFormat(type, width, height);
115 | String resolution = width + "x" + height;
116 | Log.i(TAG, "Prepare video info: " + this.formatVideoEncoder.toString() + ", resolution=" + resolution);
117 | videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, this.formatVideoEncoder.getFormatCodec());
118 | videoFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
119 | videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
120 | videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
121 | videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
122 | //Set CBR mode if supported by encoder.
123 | if (CodecUtil.isCBRModeSupported(encoder, type)) {
124 | // 定码率
125 | Log.i(TAG, "set bitrate mode CBR");
126 | videoFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
127 | } else if (CodecUtil.isVBRModeSupported(encoder, type)) {
128 | // 变码率
129 | Log.i(TAG, "set bitrate mode VBR");
130 | videoFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
131 | } else {
132 | Log.i(TAG, "bitrate mode CBR and VBR all not supported using default mode");
133 | }
134 | if (this.avcProfile > 0) {
135 | // MediaFormat.KEY_PROFILE, API > 21
136 | videoFormat.setInteger("profile", this.avcProfile);
137 | }
138 | if (this.avcProfileLevel > 0) {
139 | // MediaFormat.KEY_LEVEL, API > 23
140 | videoFormat.setInteger("level", this.avcProfileLevel);
141 | }
142 | setCallback();
143 | codec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
144 | running = false;
145 | if (formatVideoEncoder == FormatVideoEncoder.SURFACE) {
146 | inputSurface = codec.createInputSurface();
147 | }
148 | Log.i(TAG, "prepared");
149 | prepared = true;
150 | return true;
151 | } catch (Exception e) {
152 | Log.e(TAG, "Create VideoEncoder failed.", e);
153 | this.stop();
154 | return false;
155 | }
156 | }
157 |
158 | @Override
159 | public void start(boolean resetTs) {
160 | shouldReset = resetTs;
161 | if (resetTs) {
162 | fpsLimiter.setFPS(fps);
163 | }
164 | Log.i(TAG, "started");
165 | }
166 |
167 | @Override
168 | protected void stopImp() {
169 | if (inputSurface != null) inputSurface.release();
170 | inputSurface = null;
171 | Log.i(TAG, "stopped");
172 | }
173 |
174 | @Override
175 | public void reset() {
176 | stop(false);
177 | prepareVideoEncoder(width, height, fps, bitRate, iFrameInterval, formatVideoEncoder, avcProfile, avcProfileLevel);
178 | restart();
179 | }
180 |
181 | /**
182 | * mediaCodecList: [2135033992, 19, 21, 20, 39, 2130708361(COLOR_FormatSurface)]
183 | * 一般2135033992(COLOR_FormatYUV420Flexible)都是支持的,但我目前没有找到合适的转换方法
184 | * 21对应的是{@link android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar}
185 | * 2135033992对应的是{@link android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible}
186 | * 有限选择21、19,很多设备只有[]两种,另外两种转换可能会花屏
187 | * 虽然很多设备看起来支持很多颜色,但是最终转换的时候可能会花屏或者直接卡死
188 | */
189 | private FormatVideoEncoder chooseColorDynamically(MediaCodecInfo mediaCodecInfo) {
190 | int[] colorFormats = mediaCodecInfo.getCapabilitiesForType(type).colorFormats;
191 | Log.i(TAG, "Color supported by this encoder: " + Arrays.toString(colorFormats));
192 | // 做一个优先选择排序
193 | FormatVideoEncoder[] preferredOrder = {
194 | FormatVideoEncoder.YUV420_SEMI_PLANAR,
195 | FormatVideoEncoder.YUV420_PACKED_SEMI_PLANAR,
196 | FormatVideoEncoder.YUV420_PACKED_PLANAR,
197 | // 19,很多华为上存在问题,放最后
198 | FormatVideoEncoder.YUV420_PLANAR,
199 | };
200 | for (FormatVideoEncoder format : preferredOrder) {
201 | for (int color : colorFormats) {
202 | if (color == format.getFormatCodec()) {
203 | return format;
204 | }
205 | }
206 | }
207 | return null;
208 | }
209 |
210 | /**
211 | * Prepare encoder with default parameters
212 | */
213 | public boolean prepareVideoEncoder() {
214 | return prepareVideoEncoder(width, height, fps, bitRate, iFrameInterval, formatVideoEncoder, avcProfile, avcProfileLevel);
215 | }
216 |
217 | public void setVideoBitrateOnFly(int bitrate) {
218 | if (isRunning()) {
219 | this.bitRate = bitrate;
220 | Bundle bundle = new Bundle();
221 | bundle.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
222 | try {
223 | codec.setParameters(bundle);
224 | } catch (IllegalStateException e) {
225 | Log.e(TAG, "encoder need be running", e);
226 | }
227 | }
228 | }
229 |
230 | public Surface getInputSurface() {
231 | return inputSurface;
232 | }
233 |
234 | public void setInputSurface(Surface inputSurface) {
235 | this.inputSurface = inputSurface;
236 | }
237 |
238 | public int getWidth() {
239 | return width;
240 | }
241 |
242 | public int getHeight() {
243 | return height;
244 | }
245 |
246 | public void setFps(int fps) {
247 | this.fps = fps;
248 | }
249 |
250 | public int getFps() {
251 | return fps;
252 | }
253 |
254 | public int getBitRate() {
255 | return bitRate;
256 | }
257 |
258 | public String getType() {
259 | return type;
260 | }
261 |
262 | public void setType(String type) {
263 | this.type = type;
264 | }
265 |
266 | public FormatVideoEncoder getFormatVideoEncoder() {
267 | return formatVideoEncoder;
268 | }
269 |
270 | /**
271 | * choose the video encoder by mime.
272 | */
273 | @Override
274 | protected MediaCodecInfo chooseEncoder(String mime) {
275 | List mediaCodecInfoList;
276 | if (force == CodecUtil.Force.HARDWARE) {
277 | mediaCodecInfoList = CodecUtil.getAllHardwareEncoders(mime, true);
278 | } else if (force == CodecUtil.Force.SOFTWARE) {
279 | mediaCodecInfoList = CodecUtil.getAllSoftwareEncoders(mime, true);
280 | } else {
281 | //Priority: hardware CBR > hardware > software CBR > software
282 | mediaCodecInfoList = CodecUtil.getAllEncoders(mime, true, true);
283 | }
284 |
285 | Log.i(TAG, force + ", " + mediaCodecInfoList.size() + " video encoders found");
286 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
287 | Log.i(TAG, "Encoder: " + mediaCodecInfo.getName());
288 | Log.i(TAG, "Color supported by this encoder: " + Arrays.toString(mediaCodecInfo.getCapabilitiesForType(mime).colorFormats));
289 | }
290 |
291 | /*int[] preferredColorOrder = {
292 | FormatVideoEncoder.YUV420_SEMI_PLANAR.getFormatCodec(),
293 | FormatVideoEncoder.YUV420_PLANAR.getFormatCodec(),
294 | FormatVideoEncoder.YUV420_PACKED_SEMI_PLANAR.getFormatCodec(),
295 | FormatVideoEncoder.YUV420_PACKED_PLANAR.getFormatCodec(),
296 | };*/
297 | List preferredColorOrder = Arrays.asList(
298 | FormatVideoEncoder.YUV420_SEMI_PLANAR.getFormatCodec(),
299 | FormatVideoEncoder.YUV420_PLANAR.getFormatCodec(),
300 | FormatVideoEncoder.YUV420_PACKED_SEMI_PLANAR.getFormatCodec(),
301 | FormatVideoEncoder.YUV420_PACKED_PLANAR.getFormatCodec()
302 | );
303 | // 如果指定颜色的话需要搜索多个编码器
304 | for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
305 | MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mime);
306 | // 是否有支持的颜色格式
307 | for (int color : codecCapabilities.colorFormats) {
308 | if (formatVideoEncoder == FormatVideoEncoder.SURFACE) {
309 | if (color == FormatVideoEncoder.SURFACE.getFormatCodec()) return mediaCodecInfo;
310 | } else {
311 | // 大于0为指定了颜色格式,需要找到支持该颜色的编码器
312 | if (formatVideoEncoder.getFormatCodec() > 0) {
313 | if (color == formatVideoEncoder.getFormatCodec()) {
314 | return mediaCodecInfo;
315 | }
316 | } else {
317 | // check if encoder support any yuv420 color
318 | // note: 一般存在多种编码器,目前没做优先选择某种,未指定的情况下返回第一种
319 | if (preferredColorOrder.contains(color)) {
320 | return mediaCodecInfo;
321 | }
322 | }
323 | }
324 | }
325 | }
326 | return null;
327 | }
328 |
329 | @Override
330 | protected Frame getInputFrame() throws InterruptedException {
331 | // 这里耗时了
332 | long start = System.currentTimeMillis();
333 | Frame frame = isRealTime && null != iFrameDataGetter ? iFrameDataGetter.getFrameData() : queue.take();
334 | long sinceGetFrame = System.currentTimeMillis() - start;
335 | //VRLogger.v("取帧耗时: " + sinceGetFrame + "ms, frame=" + frame);
336 | // 所以这里可能会刚好已经停止了
337 | if (frame == null) return null;
338 | // 跟当前帧理应的时间差,再减去一个处理时间
339 | long diffTime = fpsLimiter.limitFPS() - sinceGetFrame;
340 | if (diffTime > 0 && running) {
341 | SystemClock.sleep(diffTime);
342 | //VRLogger.v("frame limit discarded, sleepTime=" + diffTime + "ms");
343 | return getInputFrame();
344 | }
345 | // 上一帧时间应为取帧时的时间
346 | fpsLimiter.setLastFrameTime(start);
347 | return frame;
348 | }
349 |
350 | @Override
351 | protected long calculatePts(Frame frame, long presentTimeUs) {
352 | return Math.max(0, frame.getTimeStamp() - presentTimeUs);
353 | // return frame.getTimeStamp();
354 | }
355 |
356 | @Override
357 | public void formatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {
358 | getVideoData.onVideoFormat(mediaFormat);
359 | }
360 |
361 | @Override
362 | protected void checkBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
363 | fixTimeStamp(bufferInfo);
364 | if (formatVideoEncoder == FormatVideoEncoder.SURFACE) {
365 | // 感觉surface的方式可以理解为和实时的模式是一样的
366 | bufferInfo.presentationTimeUs = System.nanoTime() / 1000 - presentTimeUs;
367 | }
368 | }
369 |
370 | @Override
371 | protected void sendBuffer(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
372 | getVideoData.getVideoData(byteBuffer, bufferInfo);
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/lib-view-record/src/test/java/io/keyss/view_record/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package io.keyss.view_record
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url 'https://jitpack.io' }
14 | }
15 | }
16 | rootProject.name = "ViewRecord"
17 | include ':app'
18 | include ':lib-view-record'
19 |
--------------------------------------------------------------------------------