FFmepg-- 38-Jitter Buffer固定值c程序分析

文章目录

目标延迟时间

在 Jitter Buffer(抖动缓冲)的实现中,TARGET_DELAY_MS(目标延迟)是最关键的控制参数。它决定了播放器在收到第一个数据包后,需要等待多久才开始播放,以及在播放过程中维持多大的安全缓冲区。

TARGET_DELAY_MS 物理含义

可以将 TARGET_DELAY_MS 想象成一个"蓄水池"的水位线:

  • 高水位线(TARGET_DELAY_MS 大):蓄水池很深。水流(数据包)即使忽大忽小(网络抖动),池子里也总有水,出水口(播放)非常稳定。缺点是水从流入到流出需要很长时间(高延迟)。

  • 低水位线(TARGET_DELAY_MS 小):蓄水池很浅。水流进来几乎立刻流出去(低延迟)。缺点是上游水流稍微断一下(网络卡顿),池子立马见底,出水口就会断流(卡顿/丢包)。

数学定义

对于序列号为 N N N 的数据包,其理论播放时间(Playout Time)计算公式为:

T p l a y ( N ) = T b a s e + ( T S N − T S b a s e ) × 1000 S a m p l e R a t e T_{play}(N) = T_{base} + (TS_N - TS_{base}) \times \frac{1000}{SampleRate} Tplay(N)=Tbase+(TSN−TSbase)×SampleRate1000

其中:

  • T b a s e T_{base} Tbase(基准播放时间):
    T b a s e = T a r r i v a l ( F i r s t P a c k e t ) + T A R G E T _ D E L A Y _ M S T_{base} = T_{arrival}(FirstPacket) + TARGET\_DELAY\_MS Tbase=Tarrival(FirstPacket)+TARGET_DELAY_MS
  • T S TS TS 是 RTP 时间戳。

c语言实现

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h> // 用于 usleep

//gcc -std=c99 jitter_buffer_demo.c -o demo
// ./demo

//

// ================= 配置常量 =================
#define TARGET_DELAY_MS     10       // 延迟
#define FRAME_DURATION_MS   20
#define SAMPLE_RATE         8000
#define TS_STEP             160   // 20ms * 8000 / 1000
#define SLEEP_INTERVAL_US   5000  // 5ms (模拟时间片)

// ================= 数据结构 =================

// RTP 包结构
typedef struct {
    uint16_t seq;
    uint32_t rtp_ts;
    uint64_t arrive_time_ms;
    int payload_size;
} RtpPacket;

// 简单的队列节点
typedef struct QueueNode {
    RtpPacket packet;
    struct QueueNode* next;
} QueueNode;


typedef struct {
    QueueNode* head;
    QueueNode* tail;
    int size;
    
    bool is_playing;
    uint32_t next_play_ts; // 其实这个变量可以不用了,直接用 start_rtp_ts 推算
    uint64_t play_start_sys_time;
    uint32_t start_rtp_ts; // 【新增】记录播放起始对应的 RTP 时间戳
} JitterBuffer;

// 全局模拟时间
uint64_t g_mock_sys_time_ms = 1000000;

uint64_t get_current_time_ms() {
    return g_mock_sys_time_ms;
}

// ================= 队列操作 (辅助函数) =================

void jb_init(JitterBuffer* jb) {
    jb->head = NULL;
    jb->tail = NULL;
    jb->size = 0;
    jb->is_playing = false;
    jb->next_play_ts = 0;
    jb->play_start_sys_time = 0;
}

void jb_push(JitterBuffer* jb, RtpPacket pkt) {
    QueueNode* node = (QueueNode*)malloc(sizeof(QueueNode));
    if (!node) return; // 内存分配失败
    
    node->packet = pkt;
    node->next = NULL;
    
    if (jb->tail == NULL) {
        jb->head = node;
        jb->tail = node;
    } else {
        jb->tail->next = node;
        jb->tail = node;
    }
    jb->size++;
}

bool jb_pop(JitterBuffer* jb, RtpPacket* out_pkt) {
    if (jb->head == NULL) return false;
    
    QueueNode* temp = jb->head;
    *out_pkt = temp->packet; // 拷贝数据
    
    jb->head = jb->head->next;
    if (jb->head == NULL) {
        jb->tail = NULL;
    }
    
    free(temp);
    jb->size--;
    return true;
}

bool jb_peek(JitterBuffer* jb, RtpPacket* out_pkt) {
    if (jb->head == NULL) return false;
    *out_pkt = jb->head->packet;
    return true;
}

void jb_cleanup(JitterBuffer* jb) {
    while (jb->head != NULL) {
        QueueNode* temp = jb->head;
        jb->head = jb->head->next;
        free(temp);
    }
}
void on_rtp_packet(JitterBuffer* jb, RtpPacket pkt) {
    // 记录到达时间
    pkt.arrive_time_ms = get_current_time_ms();
    
    printf("[NET] 收到包 Seq:%d | RTP_TS:%u | 实际到达:%lu\n", 
           pkt.seq, pkt.rtp_ts, (unsigned long)pkt.arrive_time_ms);
    
    jb_push(jb, pkt);
    
    // 【关键逻辑】只有当还没开始播放,且缓冲区至少有 2 个包(或达到阈值)时才启动
    // 注意:这里我们强制要求至少攒够 TARGET_DELAY_MS 对应的包数量
    int threshold = (TARGET_DELAY_MS / FRAME_DURATION_MS) + 1; // 稍微多攒一点
    
  
    if (!jb->is_playing && jb->size >= threshold) {
        jb->is_playing = true;
        RtpPacket first;
        if (jb_peek(jb, &first)) {
            jb->play_start_sys_time = first.arrive_time_ms + TARGET_DELAY_MS;
            jb->start_rtp_ts = first.rtp_ts; // 【新增】记录基准 RTP 时间
            printf(">>> [INIT] 播放启动! 基准时间:%lu | 起始TS:%u\n", 
                   (unsigned long)jb->play_start_sys_time, jb->start_rtp_ts);
        }
    }
}



bool process_playout(JitterBuffer* jb) {
    if (!jb->is_playing || jb->head == NULL) return false;
    
    RtpPacket head_pkt;
    if (!jb_peek(jb, &head_pkt)) return false;
    
    // 【核心修正】直接计算相对于起始时间戳的偏移
    // 防止回绕处理 (假设 RTP TS 不会在短时间剧烈回绕)
    uint32_t ts_diff = head_pkt.rtp_ts - jb->start_rtp_ts;
    if (ts_diff > 1000000000u) ts_diff = 0; // 简单防回绕
    
    // 将 RTP 时间差转换为毫秒
    int time_offset_ms = (ts_diff * 1000) / SAMPLE_RATE;
    
    uint64_t target_play_time = jb->play_start_sys_time + time_offset_ms;
    uint64_t now = get_current_time_ms();
    
    if (now >= target_play_time) {
        RtpPacket play_pkt;
        jb_pop(jb, &play_pkt);
        
        int delay = (int)(now - target_play_time);
        if (delay > 10) {
            printf("!!! [LATE] 包迟到 %dms! Seq:%d\n", delay, play_pkt.seq);
        } else {
            printf(">>> [PLAY] 正在播放 -> Seq:%d (RTP_TS:%u) @系统时间:%lu (偏差:%dms)\n", 
                   play_pkt.seq, play_pkt.rtp_ts, (unsigned long)now, delay);
        }
        return true;
    }
    
    return false;
}


int main() {
    printf("=== SIP 网关 Jitter Buffer 模拟 (C 语言版 - 修正时间逻辑) ===\n");
    
    JitterBuffer jb;
    jb_init(&jb);
    
    // 准备数据包
    RtpPacket p1 = {1, 1000, 0, 160};
    RtpPacket p2 = {2, 1160, 0, 160};
    RtpPacket p3 = {3, 1320, 0, 160}; 
    RtpPacket p4 = {4, 1480, 0, 160};
    RtpPacket p5 = {5, 1640, 0, 160};
    RtpPacket p6 = {6, 1800, 0, 160};
    
    // 定义每个包"应该"在什么全局时间点到达 (模拟网络抖动)
    // 注意:这里单位是 ms,相对于起始时间 1000000
    // 包 1: 0ms, 包 2: 25ms (迟到5ms), 包 3: 90ms (迟到50ms!), 包 4-6: 正常
    uint64_t arrival_offsets[] = {0, 25, 90, 95, 100, 105};
    RtpPacket* packets[] = {&p1, &p2, &p3, &p4, &p5, &p6};
    bool sent[6] = {false};
    
    uint64_t start_time = 1000000;
    g_mock_sys_time_ms = start_time;
    
    printf("\n开始模拟 (纯软件时间推进)...\n\n");
    
    // 模拟总时长 150ms
    for (int offset = 0; offset <= 150; offset++) {
        // 1. 更新全局模拟时间
        g_mock_sys_time_ms = start_time + offset;
        
        // 2. 检查是否有包在这个时刻到达
        for (int i = 0; i < 6; i++) {
            if (!sent[i] && offset >= arrival_offsets[i]) {
                on_rtp_packet(&jb, *packets[i]);
                sent[i] = true;
            }
        }
        
        // 3. 尝试播放 (每 1ms 检查一次,或者每 5ms 检查一次)
        // 这里为了演示清晰,我们每次循环都检查
        process_playout(&jb);
        
        // 【关键修改】不再依赖 usleep,而是直接由 for 循环控制时间步进
        // 如果需要让程序跑得慢一点让人眼能看清,可以加一个真实的短延时,但不影响逻辑时间
        // usleep(1000); // 可选:真实延时 1ms,仅为了让人眼看日志不刷屏太快
    }
    
    // 处理剩余的包 (防止最后几个包因为循环结束没播完)
    printf("\n--- 清理剩余缓冲区 ---\n");
    while (jb.size > 0) {
        process_playout(&jb);
        g_mock_sys_time_ms += 20; // 假设时间继续走
    }

    printf("\n=== 演示结束 ===\n");
    printf("观察重点:\n");
    printf("1. 包 3 在 offset=90 时才到达 (严重延迟)。\n");
    printf("2. 但播放时,Seq 2 和 Seq 3 之间的时间间隔应接近 20ms (平滑)。\n");
    printf("3. 如果包迟到太多超过缓冲阈值,会看到 [LATE] 警告。\n");
    
    jb_cleanup(&jb);
    return 0;
}

运行效果:

root@marvella9 tzy\]# gcc -std=c99 jitter_buffer_demo.c -o demo \[root@marvella9 tzy\]# ./demo ##### 测试1 **#define TARGET_DELAY_MS 10 // 延迟** Seq 3 和 Seq 4 被判定为"彻底迟到",直接被代码逻辑丢弃(Drop)了,没有进入播放队列。 **运行结果:** ```bash === SIP 网关 Jitter Buffer 模拟 (C 语言版 - 修正时间逻辑) === 开始模拟 (纯软件时间推进)... [NET] 收到包 Seq:1 | RTP_TS:1000 | 实际到达:1000000 >>> [INIT] 播放启动! 基准时间:1000010 | 起始TS:1000 >>> [PLAY] 正在播放 -> Seq:1 (RTP_TS:1000) @系统时间:1000010 (偏差:0ms) [NET] 收到包 Seq:2 | RTP_TS:1160 | 实际到达:1000025 >>> [PLAY] 正在播放 -> Seq:2 (RTP_TS:1160) @系统时间:1000030 (偏差:0ms) [NET] 收到包 Seq:3 | RTP_TS:1320 | 实际到达:1000090 !!! [LATE] 包迟到 40ms! Seq:3 [NET] 收到包 Seq:4 | RTP_TS:1480 | 实际到达:1000095 !!! [LATE] 包迟到 25ms! Seq:4 [NET] 收到包 Seq:5 | RTP_TS:1640 | 实际到达:1000100 >>> [PLAY] 正在播放 -> Seq:5 (RTP_TS:1640) @系统时间:1000100 (偏差:10ms) [NET] 收到包 Seq:6 | RTP_TS:1800 | 实际到达:1000105 >>> [PLAY] 正在播放 -> Seq:6 (RTP_TS:1800) @系统时间:1000110 (偏差:0ms) --- 清理剩余缓冲区 --- === 演示结束 === 观察重点: 1. 包 3 在 offset=90 时才到达 (严重延迟)。 2. 但播放时,Seq 2 和 Seq 3 之间的时间间隔应接近 20ms (平滑)。 3. 如果包迟到太多超过缓冲阈值,会看到 [LATE] 警告。 ``` == 分析== 包3和包4延迟丢弃 分析过程: ###### 启动阶段 TARGET_DELAY = 10ms。 收到 Seq 1(时间 0),基准时间定为 1000010。 Seq 1 播放时间:1000010,正常播放。 ###### Seq 2 阶段 RTP TS 增量 160(20ms)。 Seq 2 理论播放时间:1000010 + 20 = 1000030。 实际到达时间:1000025。 判断:到达时间 (25) \< 理论播放时间 (30),未到播放点。 结果:正常播放,日志显示 @系统时间:1000030。 ###### 危机阶段(Seq 3 \& 4) Seq 3 理论播放时间:1000030 + 20 = 1000050。 Seq 4 理论播放时间:1000050 + 20 = 1000070。 系统时间推进: * 播完 Seq 2 后,系统时间继续走。 * 1000050 时,播放器找 Seq 3 -\> 缓冲区无(Seq 3 1000090 到达)。 * 1000070 时,播放器找 Seq 4 -\> 缓冲区无(Seq 4 1000095 到达)。 逻辑推测: 当系统时间超过包的"最后容忍时间"(理论播放时间 + 容忍值),标记为 Late/Dropped。 日志中的 `!!! [LATE]` 表示包被放弃,强行插入会导致时间轴跳变(如 60ms 静音)。 ###### 恢复阶段(Seq 5) Seq 5 理论播放时间:1000070 + 20 = 1000090。 实际到达时间:1000100。 现状:系统时间已到 1000100(因空转等待)。 决策:Seq 5 晚到 10ms,但作为重新同步起点。 日志分析: `>>> [PLAY] 正在播放 -> Seq:5 ... @系统时间:1000100` 时间轴重同步(Resync),强制对齐 Seq 5 播放时刻,接受 10ms 偏差。 ##### 测试2 **#define TARGET_DELAY_MS 100 // 延迟** 每帧正常播放 **运行结果:** ```bash === SIP 网关 Jitter Buffer 模拟 (C 语言版 - 修正时间逻辑) === 开始模拟 (纯软件时间推进)... [NET] 收到包 Seq:1 | RTP_TS:1000 | 实际到达:1000000 [NET] 收到包 Seq:2 | RTP_TS:1160 | 实际到达:1000025 [NET] 收到包 Seq:3 | RTP_TS:1320 | 实际到达:1000090 [NET] 收到包 Seq:4 | RTP_TS:1480 | 实际到达:1000095 [NET] 收到包 Seq:5 | RTP_TS:1640 | 实际到达:1000100 [NET] 收到包 Seq:6 | RTP_TS:1800 | 实际到达:1000105 >>> [INIT] 播放启动! 基准时间:1000100 | 起始TS:1000 >>> [PLAY] 正在播放 -> Seq:1 (RTP_TS:1000) @系统时间:1000105 (偏差:5ms) >>> [PLAY] 正在播放 -> Seq:2 (RTP_TS:1160) @系统时间:1000120 (偏差:0ms) >>> [PLAY] 正在播放 -> Seq:3 (RTP_TS:1320) @系统时间:1000140 (偏差:0ms) --- 清理剩余缓冲区 --- >>> [PLAY] 正在播放 -> Seq:4 (RTP_TS:1480) @系统时间:1000170 (偏差:10ms) >>> [PLAY] 正在播放 -> Seq:5 (RTP_TS:1640) @系统时间:1000190 (偏差:10ms) >>> [PLAY] 正在播放 -> Seq:6 (RTP_TS:1800) @系统时间:1000210 (偏差:10ms) === 演示结束 === 观察重点: 1. 包 3 在 offset=90 时才到达 (严重延迟)。 2. 但播放时,Seq 2 和 Seq 3 之间的时间间隔应接近 20ms (平滑)。 3. 如果包迟到太多超过缓冲阈值,会看到 [LATE] 警告。 ``` #### 总结 每一帧都是从缓冲区取的。这是铁律。 理论时间是播放线程去缓冲区"取货"的时刻表。 实际时间是网络线程往缓冲区"进货"的时刻。 Jitter Buffer 的作用就是让"进货"的不稳定性,不影响"取货"的稳定性。 如果进货太慢(网络卡),导致取货时刻缓冲区没货,就会发生卡顿或丢包(如你的 Seq 3, 4)

相关推荐
网硕互联的小客服2 小时前
服务器防火墙是如何区分正常流量和攻击流量?
运维·服务器·网络
安当加密2 小时前
基于 RADIUS 的 Linux 服务器双因子认证:从 FreeRADIUS 到轻量级 ASP 方案的演进
linux·运维·服务器
简佐义的博客2 小时前
转录组数据分析实战,仅需99元(视频版)
大数据·人工智能·数据挖掘·数据分析·音视频
斯幽柏雷科技2 小时前
【Unity】解决Win10无法播放H265(HEVC)视频
音视频
ai产品老杨2 小时前
打破异构算力壁垒:基于GB28181/RTSP与Docker容器化的企业级AI视频平台架构解析(附源码交付方案)
人工智能·docker·音视频
AI科技2 小时前
清唱歌词的音频变完整歌曲,原创音乐人用AI编曲软件作编曲伴奏一步到位
人工智能·音视频
木斯佳3 小时前
前端八股文面经大全:字节跳动音视频前端一面·下(2026-03-03)·面经深度解析
前端·音视频·状态模式
西西学代码3 小时前
Flutter---路由与导航
服务器·前端·javascript
EasyDSS3 小时前
音视频技术迭代下EasyDSS直播点播视频会议能力的发展方向与价值升级
音视频·webrtc·语音识别·点播技术·流媒体直播