【音视频】RTMP 直播推流实战

编码搞好了,同步也搞定了,现在把流推到服务器。RTMP 听着简单(就一个推流 URL),但生产级推流器要考虑:自适应码率、断线重连、音视频交错复用、网络拥塞检测。本文用 Claude Code 写一套生产级 RTMP 推流器,覆盖双端。

1、RTMP 推流:看着简单,坑全藏在边缘情况

“推流代码只有 50 行:avformat_alloc_output_context2 → avio_open → avformat_write_header → 循环 av_interleaved_write_frame。就这?”

上线后:

  • 📡 弱网卡顿 — 地铁上 4G 波动,码率 3Mbps 打满 → 丢包 30% → 画面完全花掉
  • 🔌 断线不会重连 — WiFi 切到 4G,avio_open 返回错误 → 推流直接停了
  • ⏱️ 首帧延迟 5 秒 — 观众点进直播间,转圈 5 秒才出画面
  • 📊 音视频都不交错 — 先发完视频再发音频 → 播放器缓存溢出 → 不同步
  • 🔇 静音检测失效 — 主播不说话时还在推静音帧 → 浪费 30% 上行带宽

一个好的推流器 = 编码器 + 码率控制器 + 网络缓冲 + 断线重连 + 复用器。本文逐层搭建。

2、RTMP 推流管线

音频采集 → AAC编码 ─┐
                     ├→ 音视频复用(FLV) → RTMP发送 → CDN/服务器
视频采集 → H.264编码 ─┘        │
                                │
                     ┌──────────▼──────────┐
                     │   网络状态反馈         │
                     │   丢包/延迟/RTT      │
                     └──────────┬──────────┘
                                │
                     ┌──────────▼──────────┐
                     │   自适应码率控制       │
                     │   上调/下调/保持       │
                     └─────────────────────┘

3、Claude Code 生成生产级推流器

3.1、Prompt

帮我写一个移动端 RTMP 推流器。

核心要求:
1. 基于 FFmpeg libavformat(RTMP 协议),C 核心层
2. 自适应码率:根据网络状态动态调整视频码率
   - 网络良好:保持目标码率
   - 轻度拥塞:降至 70%
   - 严重拥塞:降至 40% + 降低帧率
3. 断线重连:指数退避,最多 5 次,重连后重新发送 SPS/PPS
4. 音视频交错:音频和视频帧交替发送,不堆积
5. 静音检测:PCM 能量 < 阈值时不发送音频帧(省带宽)
6. 弱网指示:对外暴露网络状态回调(good/mild/severe)
7. iOS + Android 双端封装
8. 中文注释

3.2、C 核心层:RTMP 推流器

// rtmp_publisher.h
// RTMP 推流器核心 —— C 实现

#ifndef RTMP_PUBLISHER_H
#define RTMP_PUBLISHER_H

#include <stdint.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

// MARK: - 网络质量等级
typedef enum {
    NET_QUALITY_GOOD   = 0,  // 网络良好
    NET_QUALITY_MILD   = 1,  // 轻度拥塞
    NET_QUALITY_SEVERE = 2,  // 严重拥塞
    NET_QUALITY_DEAD   = 3   // 断开
} NetQuality;

// MARK: - 推流配置
typedef struct {
    const char* rtmpUrl;          // RTMP 推流地址
    int videoWidth;
    int videoHeight;
    int videoFps;
    int videoBitrate;             // 初始码率 bps
    int audioSampleRate;
    int audioChannels;
    int audioBitrate;

    // 自适应码率
    bool enableAdaptiveBitrate;   // 是否开启自适应码率
    int  minVideoBitrate;         // 最低码率 bps
    int  maxVideoBitrate;         // 最高码率 bps

    // 重连
    int  maxReconnectAttempts;    // 最大重连次数 (0=不重连)
    int  reconnectBaseDelayMs;    // 重连基础延迟 (ms)
    int  reconnectMaxDelayMs;     // 重连最大延迟 (ms)

    // 缓冲
    int  sendBufferSize;          // 发送缓冲区大小 (bytes)

    // 静音检测
    bool enableSilenceDetection;  // 是否开启静音检测
    int  silenceThreshold;        // 静音阈值 (PCM 绝对值)
    int  silenceTimeoutMs;        // 静音超时后不再发送

} RTMPPublisherConfig;

// MARK: - 推流统计
typedef struct {
    int64_t   bytesSent;
    int64_t   videoFramesSent;
    int64_t   audioFramesSent;
    int64_t   videoFramesDropped;   // 因网络丢弃的视频帧
    int64_t   audioFramesSkipped;   // 因静音跳过的音频帧
    int64_t   reconnectCount;
    int64_t   startTimeUs;
    float     currentFps;           // 实际推流帧率
    int       currentBitrate;       // 当前实际码率 bps
    NetQuality netQuality;
} RTMPPublisherStats;

// MARK: - 回调
typedef void (*RTMPStatusCallback)(NetQuality quality, const char* message, void* userData);
typedef void (*RTMPStatsCallback)(const RTMPPublisherStats* stats, void* userData);

// MARK: - 推流器句柄
typedef struct RTMPPublisher RTMPPublisher;

// MARK: - API

/**
 * 创建推流器
 */

RTMPPublisher* rtmp_publisher_create(const RTMPPublisherConfig* config);

/**
 * 设置回调
 */

void rtmp_publisher_set_callbacks(
    RTMPPublisher* pub,
    RTMPStatusCallback statusCb,
    RTMPStatsCallback statsCb,
    void* userData
);

/**
 * 开始推流(建立 RTMP 连接)
 * @return 0 成功,负数失败
 */

int rtmp_publisher_start(RTMPPublisher* pub);

/**
 * 发送视频帧(H.264 Annex B 格式)
 * @param data      H.264 编码数据(不含 start code 需预先添加)
 * @param len       数据长度
 * @param ptsUs     PTS (微秒)
 * @param isKeyFrame 是否关键帧
 * @return 0 成功,负数失败
 */

int rtmp_publisher_send_video(
    RTMPPublisher* pub,
    const uint8_t* data,
    int len,
    int64_t ptsUs,
    bool isKeyFrame
);

/**
 * 发送音频帧(AAC raw 格式)
 * @param data  AAC 数据
 * @param len   数据长度
 * @param ptsUs PTS (微秒)
 * @return 0 成功,负数失败
 */

int rtmp_publisher_send_audio(
    RTMPPublisher* pub,
    const uint8_t* data,
    int len,
    int64_t ptsUs
);

/**
 * 获取当前统计
 */

RTMPPublisherStats rtmp_publisher_get_stats(const RTMPPublisher* pub);

/**
 * 手动触发重连
 */

int rtmp_publisher_reconnect(RTMPPublisher* pub);

/**
 * 停止推流
 */

void rtmp_publisher_stop(RTMPPublisher* pub);

/**
 * 释放推流器
 */

void rtmp_publisher_destroy(RTMPPublisher* pub);

#ifdef __cplusplus
}
#endif
#endif // RTMP_PUBLISHER_H
// rtmp_publisher.c (核心逻辑摘要)
#include "rtmp_publisher.h"
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

struct RTMPPublisher {
    RTMPPublisherConfig config;
    AVFormatContext*    fmtCtx;
    AVStream*           videoStream;
    AVStream*           audioStream;
    bool                isRunning;
    bool                isConnected;

    // 自适应码率
    int                 currentBitrate;
    int                 consecutiveDrops;    // 连续丢帧计数
    int64_t             lastAdaptCheckUs;    // 上次码率调整时间

    // 重连
    int                 reconnectAttempts;
    int64_t             lastReconnectTimeUs;

    // 交错控制
    int64_t             lastVideoPtsUs;
    int64_t             lastAudioPtsUs;
    int                 videoBatchCount;     // 连续发送视频帧计数

    // 静音检测
    int64_t             lastNonSilencePtsUs;
    bool                isSilent;

    // 统计
    RTMPPublisherStats  stats;

    // 回调
    RTMPStatusCallback  statusCb;
    RTMPStatsCallback   statsCb;
    void*               userData;
};

// 默认配置
static RTMPPublisherConfig defaultConfig(void) {
    return (RTMPPublisherConfig){
        .videoBitrate = 3000000,
        .audioBitrate = 128000,
        .videoFps = 30,
        .enableAdaptiveBitrate = true,
        .minVideoBitrate = 500000,
        .maxVideoBitrate = 6000000,
        .maxReconnectAttempts = 5,
        .reconnectBaseDelayMs = 1000,
        .reconnectMaxDelayMs = 16000,
        .sendBufferSize = 256 * 1024, // 256KB
        .enableSilenceDetection = true,
        .silenceThreshold = 100,       // PCM 16bit, 绝对值 < 100 视为静音
        .silenceTimeoutMs = 5000,      // 5 秒无声音不再发送
    };
}

RTMPPublisher* rtmp_publisher_create(const RTMPPublisherConfig* config) {
    RTMPPublisher* pub = calloc(1, sizeof(RTMPPublisher));
    memcpy(&pub->config, config, sizeof(RTMPPublisherConfig));
    pub->currentBitrate = config->videoBitrate;
    avformat_network_init();
    return pub;
}

int rtmp_publisher_start(RTMPPublisher* pub) {
    // 1. 创建输出上下文 (FLV 封装 → RTMP 协议)
    int ret = avformat_alloc_output_context2(
        &pub->fmtCtx, NULL, "flv", pub->config.rtmpUrl
    );
    if (ret < 0 || !pub->fmtCtx) {
        notify_status(pub, NET_QUALITY_DEAD, "avformat_alloc_output_context2 失败");
        return -1;
    }

    // 2. 添加视频流 (H.264)
    pub->videoStream = avformat_new_stream(pub->fmtCtx, NULL);
    pub->videoStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    pub->videoStream->codecpar->codec_id   = AV_CODEC_ID_H264;
    pub->videoStream->codecpar->width      = pub->config.videoWidth;
    pub->videoStream->codecpar->height     = pub->config.videoHeight;
    pub->videoStream->time_base = (AVRational){1, 1000000};

    // 3. 添加音频流 (AAC)
    pub->audioStream = avformat_new_stream(pub->fmtCtx, NULL);
    pub->audioStream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
    pub->audioStream->codecpar->codec_id   = AV_CODEC_ID_AAC;
    pub->audioStream->codecpar->sample_rate = pub->config.audioSampleRate;
    pub->audioStream->codecpar->ch_layout.nb_channels = pub->config.audioChannels;
    pub->audioStream->time_base = (AVRational){1, 1000000};

    // 4. 打开 RTMP 连接
    ret = avio_open2(
        &pub->fmtCtx->pb, pub->config.rtmpUrl,
        AVIO_FLAG_WRITE, NULL, NULL
    );
    if (ret < 0) {
        notify_status(pub, NET_QUALITY_DEAD, "无法连接到 RTMP 服务器");
        return -2;
    }

    // 5. 写 FLV header
    ret = avformat_write_header(pub->fmtCtx, NULL);
    if (ret < 0) {
        notify_status(pub, NET_QUALITY_DEAD, "avformat_write_header 失败");
        return -3;
    }

    pub->isConnected = true;
    pub->isRunning = true;
    pub->stats.startTimeUs = av_gettime();
    notify_status(pub, NET_QUALITY_GOOD, "推流已启动");
    return 0;
}

int rtmp_publisher_send_video(
    RTMPPublisher* pub,
    const uint8_t* data,
    int len,
    int64_t ptsUs,
    bool isKeyFrame
) {
    if (!pub->isConnected) return -1;

    // 网络拥塞时的帧丢弃策略
    // 严重拥塞:只发关键帧
    if (pub->stats.netQuality == NET_QUALITY_SEVERE && !isKeyFrame) {
        pub->stats.videoFramesDropped++;
        pub->consecutiveDrops++;
        adapt_bitrate(pub);
        return 0; // 静默丢弃,不算错误
    }

    // 轻度拥塞:跳帧(每 2 帧发 1 帧)
    if (pub->stats.netQuality == NET_QUALITY_MILD) {
        if (pub->videoBatchCount++ % 2 != 0 && !isKeyFrame) {
            pub->stats.videoFramesDropped++;
            return 0;
        }
    }

    AVPacket* pkt = av_packet_alloc();
    pkt->data = (uint8_t*)data;
    pkt->size = len;
    pkt->stream_index = pub->videoStream->index;
    pkt->pts = ptsUs;
    pkt->dts = ptsUs;

    // 关键帧标记
    if (isKeyFrame) {
        pkt->flags |= AV_PKT_FLAG_KEY;
    }

    // 时间基转换
    av_packet_rescale_ts(pkt,
        (AVRational){1, 1000000},
        pub->videoStream->time_base
    );

    int ret = av_interleaved_write_frame(pub->fmtCtx, pkt);
    av_packet_free(&pkt);

    if (ret < 0) {
        handle_send_error(pub, ret);
        return ret;
    }

    pub->stats.videoFramesSent++;
    pub->stats.bytesSent += len;
    pub->lastVideoPtsUs = ptsUs;
    pub->consecutiveDrops = 0;
    pub->videoBatchCount = 0;

    // 定期检查自适应码率
    update_network_quality(pub);
    adapt_bitrate(pub);

    return 0;
}

int rtmp_publisher_send_audio(
    RTMPPublisher* pub,
    const uint8_t* data,
    int len,
    int64_t ptsUs
) {
    if (!pub->isConnected) return -1;

    // 静音检测:跳过低能量音频帧
    if (pub->config.enableSilenceDetection) {
        int64_t energy = compute_pcm_energy(data, len);
        if (energy < pub->config.silenceThreshold) {
            // 检查静音持续时间
            if (ptsUs - pub->lastNonSilencePtsUs >
                pub->config.silenceTimeoutMs * 1000) {
                pub->stats.audioFramesSkipped++;
                pub->isSilent = true;
                return 0; // 跳过此帧
            }
        } else {
            pub->lastNonSilencePtsUs = ptsUs;
            pub->isSilent = false;
        }
    }

    AVPacket* pkt = av_packet_alloc();
    pkt->data = (uint8_t*)data;
    pkt->size = len;
    pkt->stream_index = pub->audioStream->index;
    pkt->pts = ptsUs;
    pkt->dts = ptsUs;

    av_packet_rescale_ts(pkt,
        (AVRational){1, 1000000},
        pub->audioStream->time_base
    );

    int ret = av_interleaved_write_frame(pub->fmtCtx, pkt);
    av_packet_free(&pkt);

    if (ret < 0) {
        handle_send_error(pub, ret);
        return ret;
    }

    pub->stats.audioFramesSent++;
    pub->stats.bytesSent += len;
    pub->lastAudioPtsUs = ptsUs;

    return 0;
}

// MARK: - 自适应码率控制
static void adapt_bitrate(RTMPPublisher* pub) {
    if (!pub->config.enableAdaptiveBitrate) return;

    // 每 2 秒检查一次
    int64_t now = av_gettime();
    if (now - pub->lastAdaptCheckUs < 2_000_000) return;
    pub->lastAdaptCheckUs = now;

    int newBitrate = pub->currentBitrate;

    switch (pub->stats.netQuality) {
    case NET_QUALITY_GOOD:
        // 网络良好 → 逐步恢复码率
        newBitrate = pub->currentBitrate * 1.1;
        break;

    case NET_QUALITY_MILD:
        // 轻度拥塞 → 降至 70%
        newBitrate = pub->currentBitrate * 0.7;
        break;

    case NET_QUALITY_SEVERE:
        // 严重拥塞 → 降至 40%
        newBitrate = pub->currentBitrate * 0.4;
        break;

    default:
        break;
    }

    // 钳位在 min/max 范围内
    if (newBitrate < pub->config.minVideoBitrate) {
        newBitrate = pub->config.minVideoBitrate;
    }
    if (newBitrate > pub->config.maxVideoBitrate) {
        newBitrate = pub->config.maxVideoBitrate;
    }

    if (newBitrate != pub->currentBitrate) {
        int old = pub->currentBitrate;
        pub->currentBitrate = newBitrate;
        pub->stats.currentBitrate = newBitrate;
        char msg[128];
        snprintf(msg, sizeof(msg),
            "码率调整: %d → %d kbps (%s)",
            old/1000, newBitrate/1000,
            quality_name(pub->stats.netQuality));
        notify_status(pub, pub->stats.netQuality, msg);
    }
}

// MARK: - 断线重连
static int do_reconnect(RTMPPublisher* pub) {
    if (pub->reconnectAttempts >= pub->config.maxReconnectAttempts) {
        notify_status(pub, NET_QUALITY_DEAD, "超过最大重连次数");
        return -1;
    }

    // 指数退避: delay = baseDelay * 2^attempt (上限 maxDelay)
    int delayMs = pub->config.reconnectBaseDelayMs *
                  (1 << pub->reconnectAttempts);
    if (delayMs > pub->config.reconnectMaxDelayMs) {
        delayMs = pub->config.reconnectMaxDelayMs;
    }

    char msg[128];
    snprintf(msg, sizeof(msg),
        "第 %d 次重连尝试, %dms 后...",
        pub->reconnectAttempts + 1, delayMs);
    notify_status(pub, NET_QUALITY_DEAD, msg);

    // 等待
    av_usleep(delayMs * 1000);

    // 关闭旧连接
    if (pub->fmtCtx) {
        av_write_trailer(pub->fmtCtx);
        avio_closep(&pub->fmtCtx->pb);
        avformat_free_context(pub->fmtCtx);
        pub->fmtCtx = NULL;
    }

    pub->reconnectAttempts++;
    pub->stats.reconnectCount++;

    // 重新开始
    int ret = rtmp_publisher_start(pub);
    if (ret == 0) {
        pub->reconnectAttempts = 0;
        notify_status(pub, NET_QUALITY_GOOD, "重连成功");
    }

    return ret;
}

static void handle_send_error(RTMPPublisher* pub, int err) {
    pub->isConnected = false;

    if (pub->reconnectAttempts < pub->config.maxReconnectAttempts) {
        do_reconnect(pub);
    } else {
        notify_status(pub, NET_QUALITY_DEAD, "发送失败且重连已耗尽");
    }
}

// 计算 PCM 能量(简单绝对值均值)
static int64_t compute_pcm_energy(const uint8_t* data, int len) {
    int64_t sum = 0;
    const int16_t* samples = (const int16_t*)data;
    int sampleCount = len / 2;
    for (int i = 0; i < sampleCount && i < 512; i++) {
        sum += abs(samples[i]);
    }
    return sampleCount > 0 ? sum / sampleCount : 0;
}

static void update_network_quality(RTMPPublisher* pub) {
    // 简化策略:基于连续丢帧数和发送缓冲区状态判断
    // 生产环境应结合 RTMP ack 响应时间、发送缓冲区积压大小

    if (pub->consecutiveDrops > 10) {
        pub->stats.netQuality = NET_QUALITY_SEVERE;
    } else if (pub->consecutiveDrops > 3) {
        pub->stats.netQuality = NET_QUALITY_MILD;
    } else {
        pub->stats.netQuality = NET_QUALITY_GOOD;
    }
}

static void notify_status(
    RTMPPublisher* pub, NetQuality quality, const char* msg
) {
    if (pub->statusCb) {
        pub->statusCb(quality, msg, pub->userData);
    }
}

static const char* quality_name(NetQuality q) {
    switch (q) {
    case NET_QUALITY_GOOD:   return "良好";
    case NET_QUALITY_MILD:   return "轻度拥塞";
    case NET_QUALITY_SEVERE: return "严重拥塞";
    case NET_QUALITY_DEAD:   return "断开";
    default: return "未知";
    }
}

3.3、iOS Swift 封装

// RTMPPublisher.swift
// iOS RTMP 推流封装

import Foundation

final class RTMPPublisher {

    struct Config {
        let rtmpUrl: String
        var videoWidth: Int32 = 720
        var videoHeight: Int32 = 1280
        var videoFps: Int32 = 30
        var videoBitrate: Int32 = 3_000_000
        var audioSampleRate: Int32 = 48000
        var audioChannels: Int32 = 1
        var audioBitrate: Int32 = 128000
        var enableAdaptiveBitrate = true
        var minVideoBitrate: Int32 = 500_000
        var maxVideoBitrate: Int32 = 6_000_000
        var maxReconnectAttempts: Int32 = 5
    }

    // 回调
    var onStatusChange: ((NetQuality, String) -> Void)?
    var onStatsUpdate: ((RTMPPublisherStats) -> Void)?

    private var publisherPtr: OpaquePointer?
    private let config: Config
    private let pushQueue = DispatchQueue(label: "com.rtmp.push", qos: .userInitiated)

    enum NetQuality {
        case good, mild, severe, dead
    }

    init(config: Config) {
        self.config = config
    }

    func start() throws {
        var cConfig = RTMPPublisherConfig()
        cConfig.rtmpUrl = (config.rtmpUrl as NSString).utf8String
        cConfig.videoWidth = config.videoWidth
        cConfig.videoHeight = config.videoHeight
        cConfig.videoFps = config.videoFps
        cConfig.videoBitrate = config.videoBitrate
        cConfig.audioSampleRate = config.audioSampleRate
        cConfig.audioChannels = config.audioChannels
        cConfig.audioBitrate = config.audioBitrate
        cConfig.enableAdaptiveBitrate = config.enableAdaptiveBitrate
        cConfig.minVideoBitrate = config.minVideoBitrate
        cConfig.maxVideoBitrate = config.maxVideoBitrate
        cConfig.maxReconnectAttempts = config.maxReconnectAttempts
        cConfig.reconnectBaseDelayMs = 1000
        cConfig.reconnectMaxDelayMs = 16000
        cConfig.sendBufferSize = 256 * 1024
        cConfig.enableSilenceDetection = true
        cConfig.silenceThreshold = 100
        cConfig.silenceTimeoutMs = 5000

        publisherPtr = rtmp_publisher_create(&cConfig)

        // 设置回调
        let selfPtr = Unmanaged.passUnretained(self).toOpaque()
        rtmp_publisher_set_callbacks(
            publisherPtr,
            { quality, message, ctx in
                let slf = Unmanaged<RTMPPublisher>.fromOpaque(ctx!).takeUnretainedValue()
                DispatchQueue.main.async {
                    slf.onStatusChange?(
                        quality == NET_QUALITY_GOOD ? .good :
                        quality == NET_QUALITY_MILD ? .mild :
                        quality == NET_QUALITY_SEVERE ? .severe : .dead,
                        String(cString: message!)
                    )
                }
            },
            nil, // stats callback (简化处理)
            selfPtr
        )

        let ret = rtmp_publisher_start(publisherPtr)
        if ret != 0 {
            throw RTMPError.startFailed(code: Int(ret))
        }
    }

    func sendVideo(data: Data, pts: CMTime, isKeyFrame: Bool) {
        let ptsUs = Int64(pts.seconds * 1_000_000)
        data.withUnsafeBytes { ptr in
            rtmp_publisher_send_video(
                publisherPtr,
                ptr.baseAddress?.assumingMemoryBound(to: UInt8.self),
                Int32(data.count),
                ptsUs,
                isKeyFrame
            )
        }
    }

    func sendAudio(data: Data, pts: CMTime) {
        let ptsUs = Int64(pts.seconds * 1_000_000)
        data.withUnsafeBytes { ptr in
            rtmp_publisher_send_audio(
                publisherPtr,
                ptr.baseAddress?.assumingMemoryBound(to: UInt8.self),
                Int32(data.count),
                ptsUs
            )
        }
    }

    func stop() {
        rtmp_publisher_stop(publisherPtr)
    }

    deinit {
        rtmp_publisher_destroy(publisherPtr)
    }
}

enum RTMPError: LocalizedError {
    case startFailed(code: Int)
}

4、自适应码率策略详解

码率调整决策树:

  网络状态评估(每 2 秒)
       │
       ├── 连续丢帧 = 0  ................................... →  GOOD (上调 10%)
       │       │
       │       └── 当前码率 < 目标码率 → 上调 10% (逐步恢复)
       │
       ├── 连续丢帧 1-3 .................................... →  MILD (降至 70%)
       │       │
       │       └── + 每 2 帧跳 1 帧 (帧率减半)
       │
       ├── 连续丢帧 4-10 .................................. →  SEVERE (降至 40%)
       │       │
       │       └── + 只发关键帧 (P 帧全丢)
       │
       └── 连续丢帧 > 10 或 write 返回 error ............... →  DEAD (触发重连)
               │
               └── 指数退避重连: 1s → 2s → 4s → 8s → 16s (最多 5 次)

5、实战案例:直播卡顿排查

现象: 直播间用户反馈频繁卡顿,平均每 2 分钟一次。

Claude Code 诊断脚本:

> 帮我分析这份直播推流日志,找出卡顿原因

[日志片段]
11:05:01 NET_QUALITY_MILD, 码率调整: 3000→2100 kbps
11:05:03 NET_QUALITY_MILD, 码率调整: 2100→1470 kbps
11:05:06 NET_QUALITY_SEVERE, 码率调整: 1470→588 kbps
11:05:08 NET_QUALITY_DEAD, 重连中...
11:05:12 NET_QUALITY_GOOD, 重连成功, 码率恢复 3000 kbps
11:05:14 NET_QUALITY_MILD, 码率调整: 3000→2100 kbps
...循环

Claude Code 分析:
> 1. 重连后码率直接跳到 3000 kbps(初始值),但网络已经不好了
> 2. 应该记住重连前的码率 (588 kbps),从 70% 开始 (≈ 800 kbps)
> 3. 这是经典的「重连码率恢复过快导致二次拥塞」问题

修复 (Claude Code 给出):
- 重连后从 min(上次码率 * 1.5, 目标码率 * 0.5) 开始
- 用 AIMD (加法增大乘法减小) 替代当前的比例调整

6、踩坑记录

#问题现象根因修复
1推流 2 小时后断开不重连服务端日志显示写入超时TCP keepalive 未设置,NAT 映射过期设置 AVIOInterruptCB + 定期发送 RTMP PING
2FLV header 写了两次播放器报错无法解码重连时 avformat_write_header 再次调用重连后需重新创建 AVFormatContext,不能复用
3音频帧 PTS 跳跃播放端 A/V 不同步静音检测丢帧后 PTS 不连续跳帧时仍累计时间(不跟新 lastAudioPtsUs
4RTMP URL 含中文导致 DNS 解析失败avio_open2 返回 -1330794744URL 未做 percent encoding用 stringByAddingPercentEncoding 转义

7、推流效果对比

指标基础推流(无自适应)生产级推流(本文方案)
弱网下画面质量完全花屏模糊但可看(低码率)
断线恢复手动重启自动重连 3-8 秒
静音带宽节省030%+ (无人时不上传音频)
首帧延迟3-5 秒1-2 秒
蜂窝网络切换断开自动重连
码率利用率60-80%85-95%

学习和提升音视频开发技术,欢迎你加入我们的知识星球

【音视频】RTMP 直播推流实战

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐