15、多媒体开发:Camera与CameraX拍照、MediaPlayer与ExoPlayer播放音视频、音频录制与播放、图片选择与裁剪

多媒体开发,说白了就是让手机能「看」能「听」能「说」。我这些年做项目,几乎每个App都绕不开拍照、播放视频、录音这些功能。今天咱们就把这几个核心模块拆开揉碎了讲清楚。

15.1 Camera与CameraX:拍照这件事

Android的相机API经历过一次大换代。早期是Camera API(已废弃),后来是Camera2,再后来Google推出了CameraX——一个基于Camera2的Jetpack库。

我个人强烈建议新项目直接用CameraX。为什么?因为Camera2的坑实在太多了。我记得有一次做扫码功能,用Camera2处理不同厂商的预览尺寸,华为、小米、OPPO各有一套逻辑,调试到崩溃。换成CameraX后,一行配置就搞定了。

15.1.1 CameraX的基本使用

先加依赖:

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"

核心流程就三步:绑定生命周期、配置预览、拍照。

// 1. 获取ProcessCameraProvider
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
    val cameraProvider = cameraProviderFuture.get()
    
    // 2. 配置预览
    val preview = Preview.Builder().build()
    preview.setSurfaceProvider(previewView.surfaceProvider)
    
    // 3. 配置拍照
    val imageCapture = ImageCapture.Builder()
        .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
        .build()
    
    // 4. 绑定到生命周期
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    cameraProvider.bindToLifecycle(
        lifecycleOwner, cameraSelector, preview, imageCapture
    )
}, ContextCompat.getMainExecutor(context))

拍照时调用:

imageCapture.takePicture(
    ContextCompat.getMainExecutor(context),
    object : ImageCapture.OnImageCapturedCallback() {
        override fun onCaptureSuccess(image: ImageProxy) {
            // 处理图片
        }
        override fun onError(exception: ImageCaptureException) {
            // 处理错误
        }
    }
)
小技巧:拍照模式选CAPTURE_MODE_MINIMIZE_LATENCY时,快门响应最快。如果对画质有要求,可以换成CAPTURE_MODE_MAXIMIZE_QUALITY。我一般做扫码用前者,做拍照功能用后者。

15.1.2 踩过的坑:预览方向与屏幕旋转

我曾经被预览方向问题折磨了两天。手机竖屏拿着,预览画面却是横的。原因很简单——CameraX默认的预览方向是传感器方向,不是屏幕方向。

解决办法:

val preview = Preview.Builder()
    .setTargetRotation(Surface.ROTATION_0)  // 根据屏幕方向设置
    .build()

嗯,这里要注意:setTargetRotation要在bindToLifecycle之前调用,否则不生效。

15.2 MediaPlayer与ExoPlayer:播放音视频

播放音视频,Android原生提供了MediaPlayer,但说实话,它太「重」了。你想想看,一个MediaPlayer实例只能播放一个文件,想切换还得reset再setDataSource。而且它对HLS、DASH这些流媒体协议支持很差。

我个人更推荐ExoPlayer。它是Google官方的开源播放器,灵活、可扩展,现在YouTube都在用。

15.2.1 MediaPlayer快速上手

虽然不推荐,但简单场景还是能用用的:

val mediaPlayer = MediaPlayer.create(context, R.raw.audio)
mediaPlayer.start()

// 监听播放完成
mediaPlayer.setOnCompletionListener {
    // 播放结束
}

// 释放资源
mediaPlayer.release()
注意:MediaPlayer必须在主线程创建和调用,但prepare()是耗时操作,建议用prepareAsync()避免ANR。我曾经在低端机上直接调prepare(),结果界面卡了3秒,用户直接差评。

15.2.2 ExoPlayer实战

加依赖:

implementation "androidx.media3:media3-exoplayer:1.2.0"
implementation "androidx.media3:media3-ui:1.2.0"

基本用法:

// 创建播放器
val player = ExoPlayer.Builder(context).build()

// 绑定到PlayerView
playerView.player = player

// 准备媒体
val mediaItem = MediaItem.fromUri(videoUrl)
player.setMediaItem(mediaItem)
player.prepare()
player.playWhenReady = true

// 释放
player.release()

ExoPlayer最强大的地方在于它的自定义能力。比如你想加个缓存策略:

val cache = SimpleCache(
    cacheDir,
    LeastRecentlyUsedCacheEvictor(maxCacheSize),
    DatabaseProvider(context)
)
val cacheDataSourceFactory = CacheDataSource.Factory()
    .setCache(cache)
    .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory())

val player = ExoPlayer.Builder(context)
    .setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory))
    .build()

这样用户第二次播放同一个视频时,直接从本地缓存加载,体验好很多。

15.3 音频录制与播放

音频这块,我做过语音聊天、录音笔记、甚至K歌App。核心就两个类:MediaRecorder(录制)和AudioTrack(播放)。

15.3.1 使用MediaRecorder录制

val recorder = MediaRecorder().apply {
    setAudioSource(MediaRecorder.AudioSource.MIC)
    setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
    setAudioSamplingRate(44100)
    setAudioBitRate(128000)
    setOutputFile(filePath)
    prepare()
    start()
}

// 停止录制
recorder.stop()
recorder.release()
关键点:录音需要动态申请RECORD_AUDIO权限。Android 6.0以上还要在运行时请求。我见过太多App因为没处理权限回调,录音文件全是空的。

15.3.2 播放音频的两种方式

简单播放用MediaPlayer,低延迟播放用AudioTrack。举个例子,做语音实时对讲时,AudioTrack更合适:

val audioTrack = AudioTrack(
    AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
        .build(),
    AudioFormat.Builder()
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setSampleRate(44100)
        .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
        .build(),
    bufferSize,
    AudioTrack.MODE_STREAM,
    AudioTrack.AUDIO_SESSION_ID_GENERATE
)

audioTrack.play()
audioTrack.write(audioData, 0, audioData.size)

15.4 图片选择与裁剪

图片选择,以前大家都用第三方库,比如PhotoPicker、Matisse。但Android 13开始,Google推出了官方的图片选择器——ActivityResultContracts.PickVisualMedia

15.4.1 使用官方图片选择器

val pickMedia = registerForActivityResult(
    ActivityResultContracts.PickVisualMedia()
) { uri ->
    // 处理选中的图片URI
    if (uri != null) {
        imageView.setImageURI(uri)
    }
}

pickMedia.launch(PickVisualMediaRequest(
    ActivityResultContracts.PickVisualMedia.ImageOnly
))

这个API的好处是不需要申请READ_MEDIA_IMAGES权限,系统帮你处理了。

15.4.2 图片裁剪

裁剪我一般用系统自带的裁剪Intent,或者用uCrop库。系统裁剪简单但功能有限:

val cropIntent = Intent("com.android.camera.action.CROP").apply {
    setDataAndType(uri, "image/*")
    putExtra("crop", "true")
    putExtra("aspectX", 1)
    putExtra("aspectY", 1)
    putExtra("outputX", 300)
    putExtra("outputY", 300)
    putExtra("return-data", true)
}
startActivityForResult(cropIntent, CROP_REQUEST_CODE)
踩坑提醒:系统裁剪Intent在不同厂商手机上表现差异巨大。小米手机裁剪后返回的data可能为null,华为手机可能不支持某些参数。我曾经在适配时加了一堆机型判断,最后还是换成了uCrop。

15.5 本章知识体系

下面这张图帮你理清多媒体开发的核心脉络:

Android多媒体开发 相机拍照 CameraX Camera2 预览 / 拍照 / 分析 / 生命周期绑定 音视频播放 ExoPlayer MediaPlayer HLS / DASH / 缓存 / 自定义渲染 音频录制播放 MediaRecorder AudioTrack PCM / AAC / 实时流 / 低延迟 图片选择裁剪 权限与适配 PickVisualMedia / uCrop / 机型适配 / 运行时权限

多媒体开发的核心就是「采集→处理→展示」这条链路。CameraX帮你简化了相机采集,ExoPlayer帮你搞定了播放展示,中间的处理环节(裁剪、编码、缓存)则需要你根据业务场景灵活组合。

好了,这一章的内容就到这里。记住一句话:能用官方库就别自己造轮子,能少写代码就少写代码。毕竟,我们的目标是高效交付,不是炫技。


公众号:蓝海资料掘金营,微信deep3321