这一节主要了解一下Compose中使用视频录制功能,在应用开发过程中,经常会用到这个视频录制及播放功能,其中主要用到是Android camera相关的依赖库。简单总结:
添加依赖
Kotlin
android {
compileSdk = 35
defaultConfig {
minSdk = 24
targetSdk = 35
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.camera:camera-core:1.3.0")
implementation("androidx.camera:camera-camera2:1.3.0")
implementation("androidx.camera:camera-lifecycle:1.3.0")
implementation("androidx.camera:camera-view:1.3.0")
implementation("androidx.camera:camera-video:1.3.0")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
implementation("androidx.compose.ui:ui:1.6.8")
implementation("androidx.compose.material3:material3:1.2.1")
implementation("androidx.activity:activity-compose:1.8.2")
}
添加权限
Kotlin
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
视频录制:
Kotlin
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun VideoRecorderScreen() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO,
android.Manifest.permission.READ_MEDIA_VIDEO
)
} else {
listOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
val permissionState = rememberMultiplePermissionsState(permissions)
var hasPermissions by remember { mutableStateOf(false) }
var isRecording by remember { mutableStateOf(false) }
var recordedVideoUri by remember { mutableStateOf<Uri?>(null) }
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
var videoCapture: VideoCapture<Recorder>? by remember { mutableStateOf(null) }
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
var recording: Recording? by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
permissionState.launchMultiplePermissionRequest()
}
LaunchedEffect(permissionState.permissions) {
hasPermissions = permissionState.permissions.all { it.status.isGranted }
}
DisposableEffect(Unit) {
onDispose {
cameraExecutor.shutdown()
recording?.stop()
}
}
if (!hasPermissions) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("请授予相机、录音和存储权限以录制视频")
}
return
}
fun startRecording() {
val videoCapture = videoCapture ?: return
val name = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
val outputOptions = MediaStoreOutputOptions
.Builder(context.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
recording = videoCapture.output
.prepareRecording(context, outputOptions)
.apply {
if (PermissionChecker.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
isRecording = true
recordedVideoUri = null
}
is VideoRecordEvent.Finalize -> {
isRecording = false
if (!recordEvent.hasError()) {
recordedVideoUri = recordEvent.outputResults.outputUri
}
}
}
}
}
fun stopRecording() {
recording?.stop()
recording = null
}
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
}
val preview = Preview.Builder().build().apply {
setSurfaceProvider(previewView.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HD))
.build()
videoCapture = VideoCapture.withOutput(recorder)
val cameraProvider = cameraProviderFuture.get()
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture
)
} catch (e: Exception) {
e.printStackTrace()
}
previewView
},
modifier = Modifier.fillMaxSize()
)
Button(
onClick = { if (isRecording) stopRecording() else startRecording() },
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(30.dp)
) {
Text(if (isRecording) "停止录制" else "开始录制")
}
//录制成功提示
recordedVideoUri?.let { uri ->
Text(
text = "录制成功:$uri",
modifier = Modifier
.align(Alignment.TopCenter)
.padding(20.dp)
)
}
}
}
视频录制+时长显示+视频预览:
Kotlin
import android.content.ContentValues
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.delay
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun VideoRecorderScreenDemo() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO,
android.Manifest.permission.READ_MEDIA_VIDEO
)
} else {
listOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
val permissionState = rememberMultiplePermissionsState(permissions)
var hasPermissions by remember { mutableStateOf(false) }
var isRecording by remember { mutableStateOf(false) }
var recordedVideoUri by remember { mutableStateOf<Uri?>(null) }
var recordingTime by remember { mutableStateOf(0) }
var isTimerRunning by remember { mutableStateOf(false) }
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
var videoCapture: VideoCapture<Recorder>? by remember { mutableStateOf(null) }
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
var recording: Recording? by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
permissionState.launchMultiplePermissionRequest()
}
LaunchedEffect(permissionState.permissions) {
hasPermissions = permissionState.permissions.all { it.status.isGranted }
}
LaunchedEffect(isTimerRunning) {
if (isTimerRunning) {
while (isRecording) {
delay(1000)
recordingTime++
}
isTimerRunning = false
}
}
DisposableEffect(Unit) {
onDispose {
cameraExecutor.shutdown()
recording?.stop()
isRecording = false
isTimerRunning = false
}
}
fun formatRecordingTime(seconds: Int): String {
val min = seconds / 60
val sec = seconds % 60
return String.format("%02d:%02d", min, sec)
}
fun playRecordedVideo(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "video/mp4")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Toast.makeText(context, "没有找到视频播放器", Toast.LENGTH_SHORT).show()
}
}
fun startRecording() {
val videoCapture = videoCapture ?: return
recordingTime = 0
isRecording = true
isTimerRunning = true
val name = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
val outputOptions = MediaStoreOutputOptions
.Builder(context.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
recording = videoCapture.output
.prepareRecording(context, outputOptions)
.apply {
if (PermissionChecker.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
isRecording = true
recordedVideoUri = null
}
is VideoRecordEvent.Finalize -> {
isRecording = false
if (!recordEvent.hasError()) {
recordedVideoUri = recordEvent.outputResults.outputUri
Toast.makeText(context, "录制成功!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "录制失败:${recordEvent.error}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
fun stopRecording() {
recording?.stop()
recording = null
isRecording = false
}
if (!hasPermissions) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("请授予相机、录音和存储权限以录制视频")
}
return
}
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
}
val preview = Preview.Builder().build().apply {
setSurfaceProvider(previewView.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HD))
.build()
videoCapture = VideoCapture.withOutput(recorder)
val cameraProvider = cameraProviderFuture.get()
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture
)
} catch (e: Exception) {
e.printStackTrace()
}
previewView
},
modifier = Modifier.fillMaxSize()
)
if (isRecording) {
Text(
text = formatRecordingTime(recordingTime),
modifier = Modifier
.align(Alignment.TopCenter)
.padding(20.dp),
color = androidx.compose.ui.graphics.Color.White
)
}
Button(
onClick = { if (isRecording) stopRecording() else startRecording() },
modifier = Modifier
.align(Alignment.BottomStart)
.padding(30.dp)
) {
Text(if (isRecording) "停止录制" else "开始录制")
}
recordedVideoUri?.let { uri ->
Button(
onClick = { playRecordedVideo(uri) },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(30.dp)
) {
Text("播放视频")
}
}
}
}
