在“Android笔记(八):基于CameraX库结合Compose和传统视图组件PreviewView实现照相机画面预览和照相功能”,文中介绍了拍照功能的实现,在本文中将介绍结合JetPack Compose和CameraX实现视频的录制。
新建一个项目
在项目中做如下处理:
在项目的模块对应的build.gradle.kt中增加如下的依赖库:
增加CameraX相关库
val camerax_version = "1.3.0-alpha04" implementation("androidx.camera:camera-core:$camerax_version") implementation("androidx.camera:camera-camera2:${camerax_version}") implementation("androidx.camera:camera-lifecycle:${camerax_version}") implementation("androidx.camera:camera-video:${camerax_version}") implementation("androidx.camera:camera-view:${camerax_version}" ) implementation("androidx.camera:camera-extensions:${camerax_version}")
增加Material3中扩展的图标库
val icon_extended_version = "1.5.4" implementation("androidx.compose.material:material-icons-extended:$icon_extended_version")
注意:因为在前面展示的Android移动应用中使用的图标可选有限,因此增加“androidx.compose.material:material-icons-extended”图标扩展库,丰富图标的选择。
视频录制需要做两个方面的处理,需要录制视频流和同时录制音频流。然后对音频流和视频流进行压缩处理,最终写入到磁盘保存起来。在图1展示的是这样工作的示意图。
图1
LifecycleCameraController
CameraX提供了LifecycleCameraController。LifecycleCameraController提供了CameraX的大部分的特性,它是一个高级的控制器类提供了CameraX的核心特性,用于处理照相机的初始化、创建和配置用例(这里的用例提供了将用例参数映射到相机的可用参数的功能,具体这些用例是指:拍照用例:CameraController.IMAGE_CAPTURE、 图像分析用例:CameraController.IMAGE_ANALYSIS、视频录制用例:CameraController.VIDEO_CAPTURE,…),并将它们绑定到一个生命周期拥有者对象。LifecycleCamera监听设备的Motion Sensor(运动感应器),设置目标的旋转参数。
val cameraController: LifecycleCameraController
= LifecycleCameraController(applicationContext).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE) //设置用例
}
CameraSelector
CameraSelector用于选择摄像头,具体选择包括:
使用 CameraSelector.DEFAULT_FRONT_CAMERA 请求默认的前置摄像头。
使用 CameraSelector.DEFAULT_BACK_CAMERA 请求默认的后置摄像头。
使用CameraSelector.Builder.addCameraFilter() 按 CameraCharacteristics 过滤可用设备列表。
Recording
Recording是实际上执行录制视频的对象,在活动录制视频时,提供暂停、恢复或停止录制的控件。如果在录制的过程中发生错误,则会初始化一个VideoRecordEvent.Finalized状态,所有的控制将进入空操作。
//创建Recording对象并启动视频录制
val recording = cameraController.startRecording(…)
在这里:startRecording根据录制得到的视频存储处理方式不同有三种形式:
(1)写入到文件
public Recording startRecording(
@NonNull FileOutputOptions outputOptions, //写入到文件的输出参数
@NonNull AudioConfig audioConfig, //音频的配置
@NonNull Executor executor,//将在其上运行事件侦听器的线程池
@NonNull Consumer listener)//处理视频录制的事件监听器
(2)根据文件描述写入到文件
public Recording startRecording(
@NonNull FileDescriptorOutputOptions outputOptions,//写入到文件描述的输出参数
@NonNull AudioConfig audioConfig,//音频的配置
@NonNull Executor executor,//将在其上运行事件侦听器的线程池
@NonNull Consumer listener)//处理视频录制的事件监听器
(3)写入到媒体库
public Recording startRecording(
@NonNull MediaStoreOutputOptions outputOptions,//写入到媒体库的输出参数
@NonNull AudioConfig audioConfig,//音频的配置
@NonNull Executor executor,//将在其上运行事件侦听器的线程池
@NonNull Consumer listener)//处理视频录制的事件监听器
class MainActivity : ComponentActivity() {
companion object{
//定义权限数组
val CAMERAX_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA, //请求相机
android.Manifest.permission.RECORD_AUDIO) //请求录制音频
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//检查权限,若无就请求权限
handlePermissions()
setContent {
val cameraController: LifecycleCameraController = remember {
LifecycleCameraController(applicationContext).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
VideoCaptureAppTheme {
MainScreen(cameraController)
}
}
}
/**
* 检查权限
* @return Boolean
*/
private fun hasRequiredPermissions():Boolean = CAMERAX_PERMISSIONS.all{
ContextCompat.checkSelfPermission(
applicationContext,
it) == PackageManager.PERMISSION_GRANTED
}
/**
* Handle permissions
* 处理权限的操作
*/
private fun handlePermissions(){
if(!hasRequiredPermissions()){
//请求权限
ActivityCompat.requestPermissions(this, CAMERAX_PERMISSIONS,0)
}
}
}
利用view视图组件PreviewView来定义视频预览的界面,同时处理将LifecycleCameraController对象与当前的生命周期对象进行绑定。由当前生命周期拥有者LocalLifecycleOwner.current的生命周期lifecycle的状态将决定摄像机何时打开、启动、停止和关闭。
@Composable
fun VideoPreview(cameraController:LifecycleCameraController,
modifier: Modifier = Modifier){
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
factory={context: Context ->
//预览视图
PreviewView(context).apply{
this.controller = cameraController
cameraController.bindToLifecycle(lifecycleOwner)
}
},
modifier = modifier
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(cameraController: LifecycleCameraController){
//设置前后摄像头的图标状态
var cameraIcon by remember{mutableStateOf(Icons.Default.VideoCameraBack)}
Scaffold {
Box(modifier = Modifier.fillMaxSize().padding(it)) {
//视频预览的界面
VideoPreview(cameraController = cameraController, modifier = Modifier.fillMaxSize())
//左上角的摄像头前后镜图标
IconButton(onClick = {
//照相机前后摄像头的切换
if (cameraController.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) {
//设置后镜头
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraIcon = Icons.Default.VideoCameraBack
} else {
//设置前镜头
cameraController.cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
cameraIcon=Icons.Default.VideoCameraFront
}
}, modifier = Modifier.offset(16.dp, 16.dp)) {
Icon(
imageVector = cameraIcon,
tint = Color.Green,
contentDescription = "摄像头"
)
}
}
}
}
下列代码将录制视频采用下列录制的方式
public Recording startRecording(
@NonNull FileOutputOptions outputOptions, //写入到文件的输出参数
@NonNull AudioConfig audioConfig, //音频的配置
@NonNull Executor executor,//将在其上运行事件侦听器的线程池
@NonNull Consumer listener)//处理视频录制的事件监听器
这样的视频文件就会保存到当前应用包下的文件目录中,具体代码如下所示:
class MainActivity : ComponentActivity() {
private var recording: Recording? = null
companion object{
val CAMERAX_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA,//请求相机
android.Manifest.permission.RECORD_AUDIO)//请求录制音频
}
@SuppressLint("UnsafeOptInUsageError")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//检查权限,若无就请求权限
handlePermissions()
setContent {
val cameraController: LifecycleCameraController = remember {
LifecycleCameraController(applicationContext).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
VideoCaptureAppTheme {
MainScreen(cameraController,this::recordVideo)
}
}
}
/**
* 检查权限
* @return Boolean
*/
private fun hasRequiredPermissions():Boolean = CAMERAX_PERMISSIONS.all{
ContextCompat.checkSelfPermission(
applicationContext,
it) == PackageManager.PERMISSION_GRANTED
}
/**
* Request permissions
* 处理权限的操作
*/
private fun handlePermissions(){
if(!hasRequiredPermissions()){
//请求权限
ActivityCompat.requestPermissions(this, CAMERAX_PERMISSIONS,0)
}
}
/**
* 录制视频
* @param cmaeraController LifecycleCameraController
*/
@SuppressLint("MissingPermission", "UnsafeOptInUsageError")
private fun recordVideo(cameraController:LifecycleCameraController){
if(recording!=null){
//停止录制视频
recording?.stop()
recording = null
return
}
if(!hasRequiredPermissions())//没有访问权限
return
val outputFile = File(filesDir,"video.mp4")
//执行视频录制
recording = cameraController.startRecording(
//输出文件配置
FileOutputOptions.Builder(outputFile).build(),
//启动语音
AudioConfig.create(true),
//在当前应用的上下文中创建事件监听器依附运行的线程池
ContextCompat.getMainExecutor(applicationContext)
){event:VideoRecordEvent->
//监听器处理器
when(event){
is VideoRecordEvent.Finalize->{
if(event.hasError()){
recording?.close()
recording = null
Toast.makeText(applicationContext,
"录制视频失败",
Toast.LENGTH_LONG).show()
}else{
Toast.makeText(applicationContext,
"录制视频成功",
Toast.LENGTH_LONG).show()
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(cameraController: LifecycleCameraController,
onRecordAction:(cameraController:LifecycleCameraController)->Unit){
//设置前后摄像头的图标状态
var cameraIcon by remember{mutableStateOf(Icons.Default.VideoCameraBack)}
var selected by remember{mutableStateOf(false)}
Scaffold(bottomBar = {
BottomAppBar {
NavigationBarItem(selected = selected,
onClick = {
onRecordAction(cameraController)
},
icon = {
Icon(imageVector = Icons.Default.Videocam,contentDescription = "视频录制")
})
}
}) {
Box(modifier = Modifier
.fillMaxSize()
.padding(it)) {
VideoPreview(cameraController = cameraController, modifier = Modifier.fillMaxSize())
IconButton(onClick = {
//照相机前后摄像头的切换
if (cameraController.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) {
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraIcon = Icons.Default.VideoCameraBack
} else {
cameraController.cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
cameraIcon=Icons.Default.VideoCameraFront
}
}, modifier = Modifier.offset(16.dp, 16.dp)) {
Icon(
imageVector = cameraIcon,
tint = Color.Green,
contentDescription = "摄像头"
)
}
}
}
}
运行结果如图4所示:
图4
启动Device Explorer设备浏览器,在:data->data->对应应用的包名->files“目录下可以发现已经录制成功的视频文件video.mp4。
图5
上述的代码需要检索并浏览录制的视频非常困难,并不是一个很好的方法。比较好的处理方式,是将录制的视频直接保存在媒体库中。通过将时间戳作为视频文件名,根据时间便可以非常方便的在移动终端的媒体库中检索和浏览录制的视频。则就需要在录制视频中采用如下的方式:
public Recording startRecording(
@NonNull MediaStoreOutputOptions outputOptions,//写入到媒体库的输出参数
@NonNull AudioConfig audioConfig,//音频的配置
@NonNull Executor executor,//将在其上运行事件侦听器的线程池
@NonNull Consumer listener)//处理视频录制的事件监听器
将上述的MainActivity中关于视频录制的私有方法recordVideo重新定义,具体代码如下:
class MainActivity : ComponentActivity() {
private var recording: Recording? = null
companion object{
val CAMERAX_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA,//请求相机
android.Manifest.permission.RECORD_AUDIO)//请求录制音频
}
@SuppressLint("UnsafeOptInUsageError")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//检查权限,若无就请求权限
handlePermissions()
setContent {
val cameraController: LifecycleCameraController = remember {
LifecycleCameraController(applicationContext).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
VideoCaptureAppTheme {
MainScreen(cameraController,this::recordVideo)
}
}
}
/**
* 检查权限
* @return Boolean
*/
private fun hasRequiredPermissions():Boolean = CAMERAX_PERMISSIONS.all{
ContextCompat.checkSelfPermission(
applicationContext,
it) == PackageManager.PERMISSION_GRANTED
}
/**
* Request permissions
* 处理权限的操作
*/
private fun handlePermissions(){
if(!hasRequiredPermissions()){
//请求权限
ActivityCompat.requestPermissions(this, CAMERAX_PERMISSIONS,0)
}
}
/**
* 录制视频到文件
* @param cmaeraController LifecycleCameraController
*/
@SuppressLint("MissingPermission", "UnsafeOptInUsageError")
private fun recordVideo(cameraController:LifecycleCameraController){
if(recording!=null){
//停止录制视频
recording?.stop()
recording = null
return
}
if(!hasRequiredPermissions())//没有访问权限
return
//定义包含时间戳的视频文件名
val name = SimpleDateFormat("yyyy-MM-dd hh:mm:ss",
Locale.CHINA).format(System.currentTimeMillis())
//定义关于一条视频记录的相关配置
val contentValue = 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 mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValue).build()
//执行视频录制
recording = cameraController.startRecording(
//输出到媒体库的配置
mediaStoreOutputOptions,
//启动语音
AudioConfig.create(true),
//在当前应用的上下文中创建事件监听器依附运行的线程池
ContextCompat.getMainExecutor(applicationContext)
){event:VideoRecordEvent->
//监听器处理器
when(event){
is VideoRecordEvent.Finalize->{
if(event.hasError()){
recording?.close()
recording = null
Toast.makeText(applicationContext,
"录制视频失败",
Toast.LENGTH_LONG).show()
}else{
Toast.makeText(applicationContext,
"录制视频成功",
Toast.LENGTH_LONG).show()
}
}
}
}
}
}
运行录制视频后,在手机模拟器的图片库中可以发现刚刚录制的视频,运行结果如图6所示。
图6
当然,模拟器的前后镜头可以自行设置webCam0,如图6所示。
图7
对于Camera Front和Camera Back可以任选其一设置为Webcam0,不能二者同时进行设置为WebCam0。通过这样的设置,可以在模拟器中启动WebCam,录制实时的视频。运行效果类似图8所示:
图8
https://developer.android.google.cn/training/camerax/video-capture?hl=zh-cn