当前位置: 首页 > news >正文

用Camera2 API实现一个简易抖音拍摄功能:录制、预览与视频保存

用Camera2 API打造短视频拍摄功能:从零实现抖音式交互体验

在移动互联网时代,短视频应用已经成为人们日常生活中不可或缺的娱乐方式。作为Android开发者,掌握如何构建一个高效、流畅的短视频拍摄功能至关重要。本文将带你深入探索如何利用Camera2 API和MediaRecorder,从零开始实现一个类似抖音的短视频拍摄模块,涵盖实时预览、录制控制、视频保存等核心功能。

1. Camera2 API基础与项目准备

Camera2 API是Android 5.0(API 21)引入的全新相机框架,相比传统的Camera API,它提供了更精细的控制和更高的性能。在开始实现短视频拍摄功能前,我们需要做好以下准备工作:

开发环境要求:

  • Android Studio最新稳定版
  • 目标API级别设置为21或更高
  • 测试设备运行Android 5.0及以上系统

关键依赖与权限配置:

在AndroidManifest.xml中添加必要的权限声明:

<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

项目结构规划:

app/ ├── java/ │ ├── com.example.videorecorder/ │ │ ├── CameraController.kt // 相机控制核心逻辑 │ │ ├── VideoRecorder.kt // 视频录制处理 │ │ ├── PreviewView.kt // 自定义预览视图 │ │ └── MainActivity.kt // 主界面 └── res/ ├── layout/ │ └── activity_main.xml // 主界面布局

Camera2 API的核心组件包括:

  • CameraManager:管理系统中的所有摄像头设备
  • CameraDevice:代表单个摄像头设备
  • CameraCharacteristics:描述摄像头设备的特性
  • CaptureRequest:定义捕获图像的参数
  • CameraCaptureSession:管理摄像头捕获会话

2. 实现相机预览功能

相机预览是短视频拍摄的基础,良好的预览体验直接影响用户的使用感受。下面我们一步步实现高质量的预览功能。

2.1 初始化相机设备

首先创建一个CameraController类来管理相机相关操作:

class CameraController(context: Context, textureView: TextureView) { private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager private val cameraId: String private var cameraDevice: CameraDevice? = null private var captureSession: CameraCaptureSession? = null private lateinit var previewRequestBuilder: CaptureRequest.Builder init { // 获取后置摄像头ID cameraId = cameraManager.cameraIdList.first { id -> cameraManager.getCameraCharacteristics(id) .get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK } } private val stateCallback = object : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { cameraDevice = camera startPreview() } override fun onDisconnected(camera: CameraDevice) { camera.close() cameraDevice = null } override fun onError(camera: CameraDevice, error: Int) { camera.close() cameraDevice = null } } fun openCamera() { if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { cameraManager.openCamera(cameraId, backgroundHandler, stateCallback) } } // 其他方法将在后续实现 }

2.2 配置预览会话

创建预览会话需要以下几个步骤:

  1. 创建SurfaceTexture并设置到TextureView
  2. 配置预览尺寸和输出Surface
  3. 创建CaptureRequest.Builder
  4. 建立CameraCaptureSession
private fun startPreview() { val texture = textureView.surfaceTexture ?: return // 设置默认缓冲区大小 val characteristics = cameraManager.getCameraCharacteristics(cameraId) val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) val previewSize = map?.getOutputSizes(SurfaceTexture::class.java)?.maxByOrNull { it.width * it.height } texture.setDefaultBufferSize(previewSize?.width ?: 1080, previewSize?.height ?: 1920) val surface = Surface(texture) previewRequestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) previewRequestBuilder.addTarget(surface) cameraDevice?.createCaptureSession( listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { captureSession = session previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) session.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler) } override fun onConfigureFailed(session: CameraCaptureSession) { Log.e(TAG, "Failed to configure capture session") } }, backgroundHandler ) }

2.3 处理生命周期和权限

在Activity中正确处理生命周期和权限请求:

class MainActivity : AppCompatActivity() { private lateinit var cameraController: CameraController private lateinit var textureView: TextureView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textureView = findViewById(R.id.texture_view) cameraController = CameraController(this, textureView) textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { if (checkPermissions()) { cameraController.openCamera() } else { requestPermissions() } } // 其他回调方法... } } private fun checkPermissions(): Boolean { return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED } private fun requestPermissions() { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), REQUEST_CODE_PERMISSIONS ) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_PERMISSIONS && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { cameraController.openCamera() } } companion object { private const val REQUEST_CODE_PERMISSIONS = 1001 } }

3. 实现视频录制功能

视频录制是短视频应用的核心功能,我们需要整合Camera2 API和MediaRecorder来实现高质量的录制体验。

3.1 配置MediaRecorder

创建一个VideoRecorder类来封装视频录制逻辑:

class VideoRecorder(private val context: Context) { private var mediaRecorder: MediaRecorder? = null private var videoFile: File? = null fun prepare(outputFile: File, width: Int, height: Int, surface: Surface): Surface { mediaRecorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setVideoEncoder(MediaRecorder.VideoEncoder.H264) setVideoEncodingBitRate(5 * 1024 * 1024) // 5Mbps setVideoFrameRate(30) setVideoSize(width, height) setOrientationHint(90) // 竖屏录制 setOutputFile(outputFile.absolutePath) setPreviewDisplay(surface) prepare() } videoFile = outputFile return mediaRecorder!!.surface } fun start() { mediaRecorder?.start() } fun stop() { mediaRecorder?.apply { stop() reset() release() } mediaRecorder = null } fun getOutputFile(): File? = videoFile }

3.2 集成录制功能到CameraController

在CameraController中添加录制相关逻辑:

class CameraController(context: Context, textureView: TextureView) { private val videoRecorder = VideoRecorder(context) private var isRecording = false fun startRecording() { if (isRecording) return val outputFile = File(context.getExternalFilesDir(null), "video_${System.currentTimeMillis()}.mp4") val recorderSurface = videoRecorder.prepare( outputFile, previewSize.width, previewSize.height, Surface(textureView.surfaceTexture) ) captureSession?.stopRepeating() captureSession?.abortCaptures() val previewRequestBuilder = cameraDevice!!.createCaptureRequest( CameraDevice.TEMPLATE_RECORD ).apply { addTarget(Surface(textureView.surfaceTexture)) addTarget(recorderSurface) set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO) } cameraDevice?.createCaptureSession( listOf(Surface(textureView.surfaceTexture), recorderSurface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { captureSession = session session.setRepeatingRequest(previewRequestBuilder.build(), null, null) videoRecorder.start() isRecording = true } override fun onConfigureFailed(session: CameraCaptureSession) { Log.e(TAG, "Failed to configure recording session") } }, null ) } fun stopRecording() { if (!isRecording) return videoRecorder.stop() isRecording = false // 重启预览 startPreview() // 保存视频到媒体库 saveVideoToGallery(videoRecorder.getOutputFile()) } private fun saveVideoToGallery(file: File?) { file ?: return val values = ContentValues().apply { put(MediaStore.Video.Media.TITLE, file.name) put(MediaStore.Video.Media.DISPLAY_NAME, file.name) put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000) put(MediaStore.Video.Media.DATA, file.absolutePath) } context.contentResolver.insert( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values ) } }

3.3 添加录制控制UI

在布局文件中添加录制控制按钮:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <TextureView android:id="@+id/texture_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/record_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="32dp" android:src="@drawable/ic_record" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

在Activity中处理录制按钮点击事件:

record_button.setOnClickListener { if (cameraController.isRecording()) { cameraController.stopRecording() record_button.setImageResource(R.drawable.ic_record) } else { cameraController.startRecording() record_button.setImageResource(R.drawable.ic_stop) } }

4. 优化用户体验与性能

一个优秀的短视频拍摄功能不仅需要实现基本功能,还需要考虑用户体验和性能优化。

4.1 添加录制计时器

在录制过程中显示已录制时间可以提升用户体验:

private var recordingStartTime = 0L private val timerHandler = Handler(Looper.getMainLooper()) private lateinit var timerTextView: TextView private val timerRunnable = object : Runnable { override fun run() { val elapsedMillis = System.currentTimeMillis() - recordingStartTime timerTextView.text = SimpleDateFormat("mm:ss", Locale.getDefault()) .format(Date(elapsedMillis)) timerHandler.postDelayed(this, 1000) } } fun startRecording() { recordingStartTime = System.currentTimeMillis() timerHandler.post(timerRunnable) // 其他录制逻辑... } fun stopRecording() { timerHandler.removeCallbacks(timerRunnable) timerTextView.text = "00:00" // 其他停止录制逻辑... }

4.2 实现闪光灯控制

添加闪光灯控制功能可以增强拍摄灵活性:

fun toggleFlash(): Boolean { val characteristics = cameraManager.getCameraCharacteristics(cameraId) val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false if (!hasFlash) return false isFlashOn = !isFlashOn previewRequestBuilder.set( CaptureRequest.FLASH_MODE, if (isFlashOn) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF ) captureSession?.setRepeatingRequest(previewRequestBuilder.build(), null, null) return isFlashOn }

4.3 优化录制性能

为了确保录制过程流畅,需要注意以下几点:

  1. 选择合适的视频参数:

    // 在VideoRecorder.prepare()中设置 setVideoEncodingBitRate(5 * 1024 * 1024) // 5Mbps适合1080p视频 setVideoFrameRate(30) // 30fps是流畅视频的标准帧率
  2. 处理设备旋转:

    override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // 重新配置相机方向 cameraController.updateOrientation() }
  3. 内存管理:

    override fun onPause() { super.onPause() if (isRecording) { stopRecording() } cameraController.closeCamera() }

4.4 添加录制进度提示

在UI中添加录制进度条可以提升用户体验:

<ProgressBar android:id="@+id/recording_progress" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="match_parent" android:layout_height="4dp" android:layout_marginHorizontal="16dp" android:max="15000" <!-- 15秒最大录制时长 --> android:progressDrawable="@drawable/progress_bar_recording" app:layout_constraintTop_toTopOf="parent" />

在录制过程中更新进度:

private val progressRunnable = object : Runnable { override fun run() { val progress = (System.currentTimeMillis() - recordingStartTime).toInt() recordingProgress.progress = progress if (progress < recordingProgress.max) { progressHandler.postDelayed(this, 50) } else { // 自动停止录制 stopRecording() } } }
http://www.jsqmd.com/news/678577/

相关文章:

  • 终极免费打字学习工具:用Qwerty Learner打造你的键盘肌肉记忆系统
  • 保姆级教程:手把手为嵌入式Linux移植NAU8810音频Codec驱动(基于ALSA ASoC框架)
  • 告别模拟器卡顿!3分钟掌握Windows原生APK安装神器
  • 从menuconfig界面反推Kconfig:一个快速定位和修改内核配置的逆向思维
  • 【UE5 Cesium实战】从本地倾斜摄影到3D场景:Cesium3DTileset全流程解析
  • 别再手动收藏了!我写了个Python脚本,自动抓取CVPR/ICCV/ECCV等顶会最新论文链接
  • Prompt Engineering实战:如何用ChatGPT API构建高效提示词模板(附LangChain代码示例)
  • 3分钟掌握ZeroOmega:跨浏览器智能代理管理的终极指南
  • Linux RT 调度器的 overloaded 标志:CPU 过载检测与处理
  • Nanbeige 4.1-3B WebUI实战教程:如何用单文件app.py实现专业级对话体验
  • 《玩转QT Designer Studio:从设计到实战》 QT Designer Studio环境搭建与核心工作区详解
  • Qianfan-OCR单卡GPU部署:避免多卡通信开销,专注视觉推理性能优化
  • 行业应用 | 从毫瓦到千瓦时,如何精准评估新能源系统的电能“吞吐量”?
  • RH850中断配置避坑指南:从TAUB定时器到CAN通信的实战代码解析
  • 【WRF-DART第2.5期】准备观测数据 (Prepare observations)
  • 别再硬编码HTML了!用Django模板+Bootstrap快速搭建企业官网(附完整源码)
  • 告别命令行:用VSCode+QEMU在Windows/Mac上图形化调试RISC-V程序(保姆级配置)
  • Ai2Psd终极指南:如何彻底解决Illustrator到Photoshop的矢量转换难题
  • Ubuntu 20.04/22.04 安装 curl 报错?别急着换源,先试试这个 apt 缓存清理命令
  • RTMDet设计精讲:大核卷积、软标签分配这些“炼丹”技巧,到底比YOLOv7强在哪?
  • 别再为Word转PDF表格变形发愁了!Aspose.Words for Java 19.5 保姆级避坑指南
  • 5个专业技巧:掌握Inter字体家族打造完美数字界面体验
  • 永磁同步电机定子槽型设计实战:从梨形槽到矩形槽的NVH优化之路
  • Real-Anime-Z保姆级教程:从Z-Image底座加载LoRA生成写实动漫风
  • 别再问怎么验证下载文件了!Windows自带的certutil命令,5分钟搞定SHA256/MD5校验
  • STM32H7复刻经典游戏:12位DAC实现4K级示波器显示
  • WindowResizer:如何轻松强制调整任何Windows窗口尺寸的完整指南
  • 从PBFT到HotStuff:一个门限签名如何把共识复杂度从O(n²)降到O(n)
  • Autolabel:如何用3步流程解决数据标注的世纪难题?
  • 离散数学面试别慌!用这20个高频考点串联集合、图论与逻辑(附速查表)