直播推流系统:Camera采集、音频采集、编码推流、断线重连

说实话,直播推流这个模块,是音视频开发里最「接地气」也最「折磨人」的一块。你想想看,手机摄像头一开,麦克风一收,数据哗哗地流到服务器,中间但凡有一个环节掉链子,观众那边就是黑屏、卡顿、音画不同步。我当年第一次做直播项目时,就因为在编码参数上少配了一个关键帧间隔,结果推流出去全是马赛克,被测试小姐姐追着骂了三天。

今天咱们就把这套流程彻底拆开。Camera怎么采、音频怎么收、编码器怎么配、推流怎么稳、断了怎么重连——一个坑一个坑地填平。

一、整体架构:直播推流的核心链路

先看一张图,把整个流程串起来。我习惯把直播推流分成四个阶段:采集、处理、编码、传输。

Camera采集 Camera2 API / Surface 音频采集 AudioRecord / MIC 视频处理 裁剪/旋转/镜像 音频处理 降噪/混音/重采样 视频编码 MediaCodec H.264 音频编码 MediaCodec AAC 推流传输 RTMP / SRT 断线重连 直播推流系统核心链路

说白了,就是手机把摄像头和麦克风的数据抓到手,经过一些预处理,扔给编码器压缩成 H.264 视频和 AAC 音频,最后打包成 RTMP 流发到服务器。如果网络断了,还得自动重连——嗯,这部分最头疼。

二、Camera采集:别再用老API了

Android 的 Camera 采集,现在主流方案是 Camera2 API。我见过不少项目还在用 Camera1,说白了就是图省事。但 Camera1 在性能、灵活性上差太多了,尤其是你要做美颜、滤镜、动态码率这些高级功能时,Camera2 才是正解。

核心要点:Camera2 基于 StateMachine 模型,通过 CaptureRequest 控制每一帧的采集参数。你需要实现 CameraDevice.StateCallback、CameraCaptureSession.CaptureCallback 两个回调。

我个人的习惯是,先打开 CameraManager,然后根据摄像头 ID 获取 CameraCharacteristics,判断是否支持你需要的输出尺寸和帧率。举个例子,你要推 1080p 30fps,就得确认摄像头支持这个组合。

// 打开摄像头示例
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
String cameraId = manager.getCameraIdList()[0]; // 通常0是后置

CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] outputSizes = map.getOutputSizes(ImageFormat.YUV_420_888);

// 选择最接近目标分辨率的尺寸
Size targetSize = new Size(1920, 1080);
Size bestSize = findBestMatch(outputSizes, targetSize);

// 创建CaptureRequest
manager.openCamera(cameraId, stateCallback, backgroundHandler);

小技巧:采集到的原始数据是 YUV_420_888 格式,但编码器通常接受 Surface 输入。你可以创建一个 SurfaceView 或 ImageReader,把 Surface 传给 MediaCodec,省去手动转换的麻烦。我在项目中就是这么干的,性能提升很明显。

三、音频采集:别忽略采样率和声道

音频采集相对简单,但坑也不少。Android 的 AudioRecord 类负责从麦克风采集 PCM 数据。你需要注意三个参数:采样率、声道数、位深。

参数 推荐值 说明
采样率 44100 Hz CD音质,兼容性最好
声道数 1 (单声道) 直播场景单声道足够,省带宽
位深 16 bit PCM 16位是标准格式
音频源 MediaRecorder.AudioSource.MIC 注意不要用VOICE_COMMUNICATION,会触发回声消除

我曾经踩过一个坑:用 VOICE_COMMUNICATION 作为音频源,结果采集到的声音像在水缸里说话一样,因为系统自动开启了回声消除和降噪。后来改成 MIC 源,问题就解决了。你想想看,直播场景下,回声消除反而会破坏音质。

// 音频采集初始化
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.MIC,
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize
);

audioRecord.startRecording();

// 在子线程中循环读取
byte[] audioBuffer = new byte[bufferSize];
while (isRecording) {
    int bytesRead = audioRecord.read(audioBuffer, 0, bufferSize);
    if (bytesRead > 0) {
        // 将PCM数据送入编码器
        feedAudioEncoder(audioBuffer, bytesRead);
    }
}

注意:AudioRecord 的 read() 方法是阻塞的,必须在子线程中调用。另外,bufferSize 不要自己随便设,用 getMinBufferSize() 获取,否则可能出现采集延迟或丢帧。

四、编码推流:MediaCodec + RTMP

编码这块,Android 官方推荐用 MediaCodec。它支持硬件编码,效率远高于软件编码。视频编码用 H.264,音频编码用 AAC,这是直播行业的标准组合。

我来说说视频编码的关键参数。码率控制模式建议用 BITRATE_MODE_VBR,因为直播场景画面变化大,VBR 能在保证画质的同时节省带宽。关键帧间隔(I帧间隔)设为 2 秒,也就是 60 帧(30fps 下),这样观众切换画面时不会等太久。

// 视频编码器配置
MediaFormat videoFormat = MediaFormat.createVideoFormat(
    MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000); // 2Mbps
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 2秒一个I帧
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);

MediaCodec videoEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
videoEncoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = videoEncoder.createInputSurface();
videoEncoder.start();

推流协议我选 RTMP,虽然老,但兼容性最好。你可以用 librtmp 或者封装好的库,比如 RTMPDump。推流时,把编码后的 H.264 NAL 单元和 AAC 数据包按照 FLV 格式封装,然后通过 RTMP 发送。

关键点:音视频同步靠时间戳。视频的 PTS(显示时间戳)和音频的 PTS 必须基于同一个时钟源。我习惯用 System.nanoTime() 作为基准,确保音画同步。

五、断线重连:直播的保命技能

直播最怕什么?断流。网络波动、服务器重启、WiFi 切换,任何一个原因都可能导致推流中断。断线重连机制,说白了就是自动检测断开,然后重新建立连接并继续推流。

我设计的重连策略是这样的:

  1. 检测断开:在推流线程中,每隔 1 秒检查 RTMP 连接状态。如果发送数据失败,或者收到服务器断开通知,立即触发重连。
  2. 指数退避:第一次重连等待 1 秒,第二次 2 秒,第三次 4 秒……最多等待 30 秒。避免频繁重连浪费资源。
  3. 最大重试次数:设置一个上限,比如 10 次。超过后停止重连,通知 UI 层显示「推流失败」。
  4. 关键帧刷新:重连成功后,立即请求编码器生成一个关键帧(I帧),这样观众端能快速恢复画面。
// 断线重连逻辑(伪代码)
private void reconnect() {
    int retryCount = 0;
    int maxRetries = 10;
    int baseDelay = 1000; // 1秒

    while (retryCount < maxRetries) {
        try {
            // 关闭旧连接
            closeRtmp();
            // 重新初始化推流器
            initRtmp(url);
            // 请求关键帧
            requestKeyFrame();
            Log.d("Reconnect", "重连成功,重试次数:" + retryCount);
            return;
        } catch (Exception e) {
            retryCount++;
            int delay = Math.min(baseDelay * (1 << retryCount), 30000);
            Log.w("Reconnect", "重连失败,等待 " + delay + "ms 后重试");
            Thread.sleep(delay);
        }
    }
    // 重连失败,通知UI
    onReconnectFailed();
}

避坑指南:我曾经遇到一个情况,重连成功后推流正常,但观众端一直黑屏。后来发现是重连后没有发送 SPS/PPS 数据。H.264 解码器需要 SPS/PPS 才能正确解码,重连后必须重新发送这两个 NAL 单元。

六、总结与经验

直播推流系统,说白了就是「采、编、推、连」四个字。Camera 采集用 Camera2,音频采集注意采样率和声道,编码用 MediaCodec 硬件加速,推流用 RTMP,断线重连用指数退避加关键帧刷新。

我做了这么多年音视频,最大的感受是:直播系统不怕功能复杂,就怕不稳定。一个断线重连没做好,用户就流失了。所以,编码参数、网络策略、异常处理,每一个细节都要反复打磨。

嗯,今天就聊到这儿。如果你在实际开发中遇到什么奇葩问题,欢迎交流。毕竟,直播这条路,坑多路远,大家一起填。


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