文章目录
目标延迟时间
在 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)