6、媒体流采集:Camera采集视频流、AudioRecord采集音频流、MediaStream的创建与管理
做视频会议APP,第一步就是搞定媒体流。说白了,就是让摄像头和麦克风乖乖听话,把采集到的数据包装成WebRTC能用的格式。我刚开始接触这块时,以为就是调几个API那么简单,结果踩了不少坑。今天咱们就把这事彻底聊透。
6.1 Camera采集视频流
Android上采集摄像头,主流方案有两个:Camera1和Camera2。我个人强烈建议直接用Camera2,虽然API复杂点,但灵活性和性能都好太多。Camera1在Android 5.0之后基本算被废弃了,你想想看,新设备上跑老API,各种兼容问题够你喝一壶的。
Camera2的核心思路是:通过CameraManager打开摄像头,然后创建CaptureSession,最后不断请求CaptureRequest来获取帧数据。嗯,这里要注意,我们不是直接拿预览画面,而是要拿到原始的NV21或YUV数据,这样才能喂给WebRTC。
我在项目中遇到过一个问题:某些华为手机上,用Camera2采集1080p时,帧率死活上不去。后来发现是FPS_RANGE没设置对,默认是15fps。所以建议你显式设置一下:
// 设置帧率范围
val fpsRange = Range(30, 30)
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange)
采集到的数据怎么传给WebRTC?关键接口是VideoCapturer。WebRTC官方提供了Camera2Capturer,但说实话,它封装得有点重。我更喜欢自己实现VideoCapturer接口,这样能精确控制采集参数。
核心要点:Camera2采集流程 = 打开CameraDevice → 创建CaptureSession → 设置ImageReader → 回调中获取帧数据 → 通过VideoSink转发给WebRTC。
6.2 AudioRecord采集音频流
音频采集相对简单,但坑也不少。Android上采集音频用AudioRecord类就够了。WebRTC对音频格式有硬性要求:16kHz采样率、16位PCM、单声道。你想想看,如果采集的是48kHz立体声,WebRTC内部还得做重采样和混音,多了一道损耗。
我习惯这样配置AudioRecord:
val sampleRate = 16000
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
val audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
注意这里用了VOICE_COMMUNICATION音源,而不是MIC。为什么?因为前者会启用回声消除和降噪,对视频会议来说至关重要。我曾经有个项目,用MIC音源采集,结果对方听到的回声特别严重,改过来之后问题就解决了。
采集线程要单独开,不能占用主线程。读取数据后,通过AudioTrack或者WebRTC的AudioSource接口传递出去。这里有个小技巧:读取循环里加个Thread.sleep(10),避免CPU空转。
避坑指南:我曾经在小米某款机型上发现,AudioRecord读取到的数据全是静音。排查了半天,发现是没申请RECORD_AUDIO权限。Android 6.0以上动态权限申请一定要做,而且要在开始采集前检查。
6.3 MediaStream的创建与管理
视频流和音频流都采集到了,怎么组合成一条完整的媒体流?这就轮到MediaStream登场了。它是WebRTC里的核心概念,说白了就是一个容器,可以装多个VideoTrack和AudioTrack。
创建MediaStream的代码很简单:
val factory = PeerConnectionFactory()
val stream = factory.createLocalMediaStream("STREAM_ID")
// 添加视频轨道
val videoSource = factory.createVideoSource(false)
val videoTrack = factory.createVideoTrack("VIDEO_TRACK_ID", videoSource)
stream.addTrack(videoTrack)
// 添加音频轨道
val audioSource = factory.createAudioSource(audioConstraints)
val audioTrack = factory.createAudioTrack("AUDIO_TRACK_ID", audioSource)
stream.addTrack(audioTrack)
这里有个细节:createVideoSource(false)中的false表示不开启屏幕共享。如果是摄像头采集,传false就行。如果是屏幕共享,才传true。
管理MediaStream时,我建议遵循以下原则:
- 生命周期要清晰:在Activity的onResume时创建,onPause时释放。别让摄像头一直开着,耗电又发热。
- 轨道ID要有意义:比如"camera_track"、"mic_track",方便调试时区分。
- 及时释放资源:调用
stream.dispose(),否则会造成内存泄漏。
你想想看,如果用户切到后台,你还在采集视频,不仅浪费资源,还可能被系统杀掉。所以一定要监听应用生命周期,动态管理MediaStream。
个人经验:我习惯把MediaStream的创建和销毁封装成一个MediaStreamManager类,内部维护状态机。这样代码清晰,也方便做重连逻辑。比如网络断开时,不销毁MediaStream,只是暂停采集,等网络恢复再继续。
6.4 整体流程梳理
说了这么多,咱们用一张图把整个流程串起来:
从图上可以清楚看到,整个流程分三步走:先分别采集视频和音频,然后各自创建Track,最后合并到MediaStream里。每一步都有它的职责,缺一不可。
6.5 常见问题与优化建议
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 摄像头画面卡顿 | 帧率设置过高或分辨率过大 | 建议720p@30fps,平衡画质和性能 |
| 音频有回声 | 未使用VOICE_COMMUNICATION音源 | 切换音源,并开启硬件回声消除 |
| MediaStream泄漏 | 未调用dispose() | 在onDestroy中统一释放所有资源 |
| 采集权限被拒 | 用户拒绝CAMERA或RECORD_AUDIO | 友好提示,引导用户去设置中开启 |
嗯,这里还要提一句:不同厂商的Android设备,Camera2的行为差异挺大的。我建议你在真机上多测试,特别是华为、小米、OPPO这些主流品牌。模拟器上跑得再好,真机也可能翻车。
好了,媒体流采集这块就聊到这儿。核心就是三件事:Camera2拿视频、AudioRecord拿音频、MediaStream把它们打包。每一步都有细节,但掌握了这些,视频会议APP的底层就稳了。