PTS、DTS、Duration与音视频同步完整示例

一、基础示例:理解时间戳

示例1:简单视频(无B帧)

视频参数:
  • 帧率:25 fps(每帧40ms)
  • 时间基:{1, 25}(每个刻度=40ms)
  • 编码顺序:I P P P P
c 复制代码
// 帧数据表
帧索引 | 帧类型 | 实际时间 | PTS(刻度) | DTS(刻度) | Duration(刻度) | 说明
-------|--------|----------|-----------|-----------|----------------|------
0      | I      | 0ms      | 0         | 0         | 1              | 关键帧
1      | P      | 40ms     | 1         | 1         | 1              | P帧
2      | P      | 80ms     | 2         | 2         | 1              | P帧
3      | P      | 120ms    | 3         | 3         | 1              | P帧
4      | P      | 160ms    | 4         | 4         | 1              | P帧

// 计算公式:
实际时间(ms) = PTS × (1000 / 25) = PTS × 40
解码顺序 = 显示顺序(因为没有B帧)

示例2:包含B帧的视频

编码顺序 ≠ 显示顺序
复制代码
显示顺序(用户看到的):I  B  B  P  B  B  P
解码顺序(编码器输出):I  P  B  B  P  B  B
c 复制代码
// 详细分析
帧索引 | 帧类型 | 显示顺序 | 解码顺序 | PTS | DTS | 实际时间 | 依赖关系
-------|--------|----------|----------|-----|-----|----------|----------
0      | I      | 0        | 0        | 0   | 0   | 0ms      | 无依赖
1      | B      | 1        | 2        | 1   | 2   | 40ms     | 依赖I和P
2      | B      | 2        | 3        | 2   | 3   | 80ms     | 依赖I和P
3      | P      | 3        | 1        | 3   | 1   | 120ms    | 依赖I
4      | B      | 4        | 5        | 4   | 5   | 160ms    | 依赖P3和P6
5      | B      | 5        | 6        | 5   | 6   | 200ms    | 依赖P3和P6
6      | P      | 6        | 4        | 6   | 4   | 240ms    | 依赖P3

// 关键观察:
1. DTS ≠ PTS(因为有B帧)
2. DTS可能小于PTS(B帧)
3. DTS可能大于PTS(P帧提前解码)
4. B帧的解码需要后面的P帧数据

示例3:音频时间戳

音频参数:
  • 采样率:44100 Hz
  • 时间基:{1, 44100}(每个刻度=1/44100秒≈0.0227ms)
  • 每帧1024个采样点
c 复制代码
// 音频帧数据
帧索引 | 采样点范围 | PTS(刻度) | 实际时间(ms) | Duration(刻度)
-------|------------|-----------|--------------|--------------
0      | 0-1023     | 0         | 0.00         | 1024
1      | 1024-2047  | 1024      | 23.22        | 1024
2      | 2048-3071  | 2048      | 46.44        | 1024
3      | 3072-4095  | 3072      | 69.66        | 1024
4      | 4096-5119  | 4096      | 92.88        | 1024

// 计算公式:
实际时间(ms) = PTS × (1000 / 44100) ≈ PTS × 0.0226757
音频帧时长 = 1024 × (1000 / 44100) ≈ 23.22ms

二、音视频同步完整流程

场景:25fps视频 + 44.1kHz音频的同步播放

初始状态:
c 复制代码
// 时间基统一为毫秒({1, 1000})
AVRational ms_timebase = {1, 1000};

// 视频参数
int video_fps = 25;
AVRational video_tb = {1, video_fps};  // {1, 25}
int64_t video_frame_duration = av_rescale_q(1, ms_timebase, video_tb);  // 40

// 音频参数  
int audio_samplerate = 44100;
AVRational audio_tb = {1, audio_samplerate};  // {1, 44100}
int audio_samples_per_frame = 1024;
int64_t audio_frame_duration = av_rescale_q(audio_samples_per_frame, 
                                           audio_tb, ms_timebase);  // ≈23

步骤1:时间戳生成

c 复制代码
// 视频时间戳生成(基于帧计数)
int64_t video_pts = frame_index;  // 0, 1, 2, 3...
int64_t video_pts_ms = av_rescale_q(video_pts, video_tb, ms_timebase);
// 结果:0ms, 40ms, 80ms, 120ms...

// 音频时间戳生成(基于采样点累加)
static int64_t total_audio_samples = 0;
int64_t audio_pts = total_audio_samples;  // 0, 1024, 2048...
total_audio_samples += audio_samples_per_frame;
int64_t audio_pts_ms = av_rescale_q(audio_pts, audio_tb, ms_timebase);
// 结果:0ms, 23.22ms, 46.44ms, 69.66ms...

步骤2:数据包时间线

c 复制代码
// 前几帧的时间线对比
帧类型 | 视频PTS(ms) | 音频PTS(ms) | 时间差(ms) | 处理策略
-------|-------------|-------------|------------|----------
V:I    | 0.00        | 0.00        | 0.00       | 同时播放
A:0    | -           | 23.22       | -          | -
V:P    | 40.00       | 46.44       | -6.44      | 视频领先
A:1    | -           | 46.44       | -          | -
V:P    | 80.00       | 69.66       | +10.34     | 音频领先
A:2    | -           | 69.66       | -          | -
V:P    | 120.00      | 92.88       | +27.12     | 音频领先更多
A:3    | -           | 92.88       | -          | -
V:P    | 160.00      | 116.10      | +43.90     | 需要干预!
A:4    | -           | 116.10      | -          | -

步骤3:同步算法实现

c 复制代码
// 同步控制器
class AVSyncController {
private:
    // 时钟基准
    int64_t master_clock_ms;      // 主时钟(通常是音频)
    int64_t video_clock_ms;       // 视频时钟
    int64_t audio_clock_ms;       // 音频时钟
    
    // 同步状态
    double sync_threshold = 40.0; // 同步阈值40ms
    double max_audio_diff = 100.0; // 最大音频差异
    
    // 统计信息
    double total_sync_error = 0;
    int frame_count = 0;
    
public:
    // 处理视频帧
    void processVideoFrame(AVFrame* frame, int64_t pts_ms) {
        video_clock_ms = pts_ms;
        
        // 计算同步误差
        double sync_error = audio_clock_ms - video_clock_ms;
        total_sync_error += fabs(sync_error);
        frame_count++;
        
        printf("视频帧: PTS=%.2fms, 音频时钟=%.2fms, 误差=%.2fms\n",
               video_clock_ms / 1000.0, audio_clock_ms / 1000.0, 
               sync_error / 1000.0);
        
        // 同步决策
        if (fabs(sync_error) > sync_threshold) {
            if (sync_error > 0) {
                // 视频落后(音频在前面)
                printf("  → 视频落后 %.2fms,需要追赶\n", sync_error / 1000.0);
                dropOrRepeatVideoFrame(frame, sync_error);
            } else {
                // 视频超前(音频在后面)
                printf("  → 视频超前 %.2fms,需要等待\n", -sync_error / 1000.0);
                delayVideoFrame(frame, -sync_error);
            }
        } else {
            // 在阈值内,正常播放
            printf("  → 同步良好,正常播放\n");
            displayVideoFrame(frame);
        }
    }
    
    // 处理音频帧
    void processAudioFrame(AVFrame* frame, int64_t pts_ms) {
        audio_clock_ms = pts_ms;
        master_clock_ms = audio_clock_ms;  // 音频作为主时钟
        
        printf("音频帧: PTS=%.2fms\n", audio_clock_ms / 1000.0);
        playAudioFrame(frame);
    }
    
    // 丢弃或重复视频帧
    void dropOrRepeatVideoFrame(AVFrame* frame, double error_ms) {
        // 计算需要调整的帧数
        double frame_time = 1000.0 / 25.0;  // 40ms/帧
        int frames_to_adjust = (int)(error_ms / frame_time);
        
        if (frames_to_adjust >= 1) {
            // 误差超过1帧,丢弃当前帧
            printf("   丢弃1帧,追赶%.0fms\n", frame_time);
            av_frame_unref(frame);
        } else {
            // 误差较小,正常显示但加快
            displayVideoFrame(frame);
        }
    }
    
    // 延迟视频帧
    void delayVideoFrame(AVFrame* frame, double error_ms) {
        if (error_ms < 10) {
            // 误差很小,微调即可
            usleep((useconds_t)(error_ms * 1000));  // 微秒
            displayVideoFrame(frame);
        } else {
            // 误差较大,重复上一帧
            printf("   重复上一帧,等待%.0fms\n", error_ms);
            repeatLastVideoFrame();
            displayVideoFrame(frame);  // 当前帧稍后显示
        }
    }
    
    // 获取同步质量报告
    void getSyncReport() {
        double avg_error = total_sync_error / frame_count;
        printf("\n=== 同步质量报告 ===\n");
        printf("总帧数: %d\n", frame_count);
        printf("平均同步误差: %.3fms\n", avg_error / 1000.0);
        printf("最大允许误差: %.1fms\n", sync_threshold / 1000.0);
        
        if (avg_error < sync_threshold * 0.5) {
            printf("同步质量: 优秀 ✓\n");
        } else if (avg_error < sync_threshold) {
            printf("同步质量: 良好 ~\n");
        } else {
            printf("同步质量: 需要改进 ✗\n");
        }
    }
};

步骤4:实际运行模拟

c 复制代码
// 模拟播放过程
int main() {
    AVSyncController sync;
    
    // 模拟数据流(交错排列)
    Packet packets[] = {
        // {类型, 时间戳(ms), 持续时间(ms)}
        {VIDEO, 0,    40},   // V0: I帧
        {AUDIO, 0,    23},   // A0
        {AUDIO, 23,   23},   // A1
        {VIDEO, 40,   40},   // V1: P帧
        {AUDIO, 46,   23},   // A2
        {AUDIO, 69,   23},   // A3
        {VIDEO, 80,   40},   // V2: P帧
        {AUDIO, 92,   23},   // A4
        {AUDIO, 115,  23},   // A5
        {VIDEO, 120,  40},   // V3: P帧
        // ... 继续更多帧
    };
    
    // 处理每个包
    for (int i = 0; i < sizeof(packets)/sizeof(packets[0]); i++) {
        if (packets[i].type == VIDEO) {
            sync.processVideoFrame(create_dummy_frame(), packets[i].timestamp);
        } else {
            sync.processAudioFrame(create_dummy_audio_frame(), packets[i].timestamp);
        }
        
        // 模拟播放时间(按持续时间)
        usleep(packets[i].duration * 1000);
    }
    
    sync.getSyncReport();
    return 0;
}

步骤5:输出结果分析

复制代码
// 程序输出示例
视频帧: PTS=0.00ms, 音频时钟=0.00ms, 误差=0.00ms
  → 同步良好,正常播放
音频帧: PTS=0.00ms
音频帧: PTS=23.22ms
视频帧: PTS=40.00ms, 音频时钟=23.22ms, 误差=-16.78ms
  → 视频超前 16.78ms,需要等待
    重复上一帧,等待17ms
音频帧: PTS=46.44ms
音频帧: PTS=69.66ms
视频帧: PTS=80.00ms, 音频时钟=69.66ms, 误差=-10.34ms
  → 视频超前 10.34ms,需要等待
    微调等待10ms
音频帧: PTS=92.88ms
音频帧: PTS=116.10ms
视频帧: PTS=120.00ms, 音频时钟=116.10ms, 误差=-3.90ms
  → 同步良好,正常播放

=== 同步质量报告 ===
总帧数: 4
平均同步误差: 7.755ms
最大允许误差: 40.0ms
同步质量: 优秀 ✓

三、B帧场景的完整解码示例

场景:解码带B帧的视频流

c 复制代码
// 输入数据包(编码器输出顺序)
Packet encoded_packets[] = {
    // {类型, PTS, DTS, 数据}
    {I_FRAME, PTS=0,  DTS=0,  data="I0"},   // 1. 先解码I帧
    {P_FRAME, PTS=3,  DTS=1,  data="P3"},   // 2. 解码P3(DTS=1)
    {B_FRAME, PTS=1,  DTS=2,  data="B1"},   // 3. 解码B1
    {B_FRAME, PTS=2,  DTS=3,  data="B2"},   // 4. 解码B2
    {P_FRAME, PTS=6,  DTS=4,  data="P6"},   // 5. 解码P6
    {B_FRAME, PTS=4,  DTS=5,  data="B4"},   // 6. 解码B4
    {B_FRAME, PTS=5,  DTS=6,  data="B5"},   // 7. 解码B5
};

// 解码器处理流程
class VideoDecoder {
private:
    std::priority_queue<Frame> decode_queue;  // 按DTS排序
    std::priority_queue<Frame> display_queue; // 按PTS排序
    
public:
    void receivePacket(Packet pkt) {
        // 1. 解码数据包
        Frame frame = decode(pkt.data);
        frame.pts = pkt.pts;
        frame.dts = pkt.dts;
        
        // 2. 放入解码队列(按DTS)
        decode_queue.push(frame);
        
        // 3. 检查是否可以显示
        checkDisplay();
    }
    
    void checkDisplay() {
        // 如果队列头的帧已经可以显示(DTS <= 当前时间)
        while (!decode_queue.empty()) {
            Frame& frame = decode_queue.top();
            
            if (frame.dts <= getCurrentTime()) {
                // 移动到显示队列(按PTS排序)
                display_queue.push(frame);
                decode_queue.pop();
            } else {
                break;
            }
        }
        
        // 显示PTS最小的帧
        if (!display_queue.empty()) {
            Frame& frame = display_queue.top();
            if (frame.pts <= getCurrentTime()) {
                displayFrame(frame);
                display_queue.pop();
            }
        }
    }
    
    void displayFrame(Frame frame) {
        printf("显示帧: PTS=%lld, DTS=%lld, 类型=%s, 数据=%s\n",
               frame.pts, frame.dts, 
               frame.type == I_FRAME ? "I" : 
               frame.type == P_FRAME ? "P" : "B",
               frame.data.c_str());
    }
};

执行过程:

复制代码
时间线 | 动作 | 解码队列(DTS) | 显示队列(PTS) | 显示输出
-------|------|---------------|---------------|----------
t=0    | 收到I0 | [I0(DTS=0)]   | []           | 
       | 显示检查 | []           | [I0(PTS=0)]  | 显示I0
t=1    | 收到P3 | [P3(DTS=1)]   | []           |
       | 显示检查 | []           | [P3(PTS=3)]  | (P3 PTS=3>1,不显示)
t=2    | 收到B1 | [B1(DTS=2)]   | []           |
       | 显示检查 | []           | [P3,B1]      | (B1 PTS=1<2,但不显示因为依赖P3)
t=3    | 收到B2 | [B2(DTS=3)]   | []           |
t=4    | 解码完成 | []           | [P3,B1,B2]   | 
       |        |               | PTS排序: B1(1),B2(2),P3(3) |
t=4    | 显示检查 |             |              | 显示B1
t=5    | 显示检查 |             |              | 显示B2
t=6    | 显示检查 |             |              | 显示P3

四、音视频同步的高级场景

场景1:快进播放

c 复制代码
class FastForwardPlayer {
private:
    double speed = 2.0;  // 2倍速
    int64_t time_base_ms = 1000;
    
public:
    // 调整时间戳以适应快进
    int64_t adjustTimestampForSpeed(int64_t original_pts_ms) {
        // 快进时:时间戳间隔变小
        int64_t adjusted = original_pts_ms / speed;
        return adjusted;
    }
    
    // 处理帧
    void processFrame(AVFrame* frame, bool is_video) {
        // 获取原始时间戳
        int64_t original_pts_ms = av_rescale_q(frame->pts, 
                                              frame->time_base,
                                              (AVRational){1, 1000});
        
        // 调整时间戳
        int64_t adjusted_pts_ms = adjustTimestampForSpeed(original_pts_ms);
        
        if (is_video) {
            // 视频:丢弃部分帧
            static int64_t last_video_pts_ms = 0;
            int64_t interval = adjusted_pts_ms - last_video_pts_ms;
            
            if (interval < (1000 / 30 / speed)) {  // 最小间隔
                // 丢弃帧
                av_frame_unref(frame);
            } else {
                displayVideoFrame(frame, adjusted_pts_ms);
                last_video_pts_ms = adjusted_pts_ms;
            }
        } else {
            // 音频:重采样改变播放速度
            resampleAudioForSpeed(frame, speed);
            playAudioFrame(frame, adjusted_pts_ms);
        }
    }
};

场景2:直播中的同步修复

c 复制代码
class LiveStreamSyncFixer {
private:
    // 时钟漂移检测
    double calculateClockDrift(int64_t audio_pts_ms, int64_t video_pts_ms) {
        static std::deque<double> recent_diffs;
        double current_diff = audio_pts_ms - video_pts_ms;
        
        recent_diffs.push_back(current_diff);
        if (recent_diffs.size() > 100) {
            recent_diffs.pop_front();
        }
        
        // 计算平均差异的变化率(时钟漂移率)
        if (recent_diffs.size() >= 10) {
            double first_avg = std::accumulate(recent_diffs.begin(), 
                                              recent_diffs.begin() + 10, 0.0) / 10;
            double last_avg = std::accumulate(recent_diffs.end() - 10,
                                             recent_diffs.end(), 0.0) / 10;
            double drift_rate = (last_avg - first_avg) / recent_diffs.size();
            return drift_rate;  // ms/帧
        }
        return 0.0;
    }
    
    // 自适应同步阈值
    double adaptiveSyncThreshold(double drift_rate) {
        double base_threshold = 40.0;  // 40ms
        
        if (fabs(drift_rate) > 0.1) {
            // 时钟漂移严重,放宽阈值
            return base_threshold * 2;
        } else if (fabs(drift_rate) > 0.01) {
            // 轻微漂移,稍微放宽
            return base_threshold * 1.5;
        } else {
            // 稳定,使用标准阈值
            return base_threshold;
        }
    }
};

五、常见问题与解决方案

问题1:音频提前耗尽

c 复制代码
// 现象:音频缓冲区空,视频还有很多帧
// 原因:视频解码慢或音频播放快

void handleAudioUnderrun() {
    // 检测音频缓冲区
    if (audio_buffer_size() < MIN_AUDIO_BUFFER) {
        // 策略1:加快视频播放
        double current_interval = video_frame_interval_ms;
        video_frame_interval_ms = std::max(current_interval * 0.9, MIN_FRAME_INTERVAL);
        
        // 策略2:如果视频超前太多,插入静音
        double av_diff = getAudioVideoDiff();
        if (av_diff < -MAX_ALLOWED_DIFF) {
            insertSilentAudio(av_diff);
        }
    }
}

问题2:时间戳跳跃

c 复制代码
// 现象:PTS突然大幅度增加或减少
// 原因:seek操作、流切换、编码问题

void handleTimestampJump(int64_t new_pts, int64_t last_pts) {
    int64_t diff = new_pts - last_pts;
    int64_t max_normal_jump = 5000;  // 5秒
    
    if (abs(diff) > max_normal_jump) {
        printf("检测到时间戳跳跃: %lldms\n", diff);
        
        if (diff > 0) {
            // 向前跳跃(可能是seek)
            printf("向前跳跃,清空缓冲区重新同步\n");
            clearBuffers();
            resetSyncState();
        } else {
            // 向后跳跃(异常)
            printf("时间戳回退,可能是编码错误\n");
            // 尝试平滑处理
            smoothTimestampTransition(last_pts, new_pts);
        }
    }
}

问题3:多轨音频同步

c 复制代码
// 场景:多语言音轨切换
class MultiAudioSync {
private:
    std::map<int, AudioTrack> audio_tracks;  // 音轨ID -> 音轨
    int active_track_id;
    
public:
    void switchAudioTrack(int new_track_id) {
        AudioTrack& old_track = audio_tracks[active_track_id];
        AudioTrack& new_track = audio_tracks[new_track_id];
        
        // 获取当前视频时间
        int64_t video_time_ms = getVideoClock();
        
        // 计算新音轨应该开始的位置
        int64_t new_audio_start_ms = video_time_ms;
        
        // 考虑音轨间的延迟差异
        int64_t track_latency_diff = new_track.decode_latency - 
                                    old_track.decode_latency;
        new_audio_start_ms += track_latency_diff;
        
        // 查找新音轨中合适的位置
        AudioFrame* start_frame = new_track.findFrameByTime(new_audio_start_ms);
        
        // 交叉淡入淡出
        crossFadeAudio(old_track, new_track, start_frame, 500);  // 500ms过渡
        
        active_track_id = new_track_id;
    }
};

六、调试技巧

1. 时间戳可视化

python 复制代码
# Python脚本:绘制音视频时间戳图
import matplotlib.pyplot as plt

def plot_av_timestamps(video_pts, audio_pts):
    plt.figure(figsize=(12, 6))
    
    # 视频时间戳
    plt.subplot(2, 1, 1)
    plt.plot(video_pts, range(len(video_pts)), 'b-', label='Video PTS')
    plt.xlabel('Time (ms)')
    plt.ylabel('Frame Index')
    plt.title('Video Timeline')
    plt.grid(True)
    plt.legend()
    
    # 音频时间戳
    plt.subplot(2, 1, 2)
    plt.plot(audio_pts, range(len(audio_pts)), 'r-', label='Audio PTS')
    plt.xlabel('Time (ms)')
    plt.ylabel('Frame Index')
    plt.title('Audio Timeline')
    plt.grid(True)
    plt.legend()
    
    # 同步误差
    plt.figure(figsize=(12, 4))
    sync_errors = []
    for i in range(min(len(video_pts), len(audio_pts))):
        error = audio_pts[i] - video_pts[i]
        sync_errors.append(error)
    
    plt.plot(sync_errors, 'g-', label='Sync Error (Audio - Video)')
    plt.axhline(y=40, color='r', linestyle='--', label='+40ms Threshold')
    plt.axhline(y=-40, color='r', linestyle='--', label='-40ms Threshold')
    plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
    plt.xlabel('Frame Index')
    plt.ylabel('Sync Error (ms)')
    plt.title('Audio-Video Synchronization Error')
    plt.grid(True)
    plt.legend()
    
    plt.show()

2. 实时监控

c 复制代码
// 实时同步监控器
class RealTimeSyncMonitor {
public:
    void logSyncStatus(double audio_time, double video_time) {
        double error = audio_time - video_time;
        
        // 统计
        static double max_error = 0;
        static double min_error = 0;
        static double total_error = 0;
        static int count = 0;
        
        max_error = std::max(max_error, error);
        min_error = std::min(min_error, error);
        total_error += fabs(error);
        count++;
        
        // 实时显示
        if (count % 100 == 0) {
            printf("[同步状态] 当前误差: %.1fms | 平均: %.1fms | 范围: [%.1f, %.1f]ms\n",
                   error, total_error/count, min_error, max_error);
            
            // 可视化ASCII图表
            plotAsciiChart(error);
        }
    }
    
    void plotAsciiChart(double error) {
        int width = 50;
        int zero_pos = width / 2;
        int error_pos = zero_pos + (int)(error / 2);  // 缩放
        
        error_pos = std::max(0, std::min(width-1, error_pos));
        
        printf("[");
        for (int i = 0; i < width; i++) {
            if (i == zero_pos) {
                printf("|");  // 零点
            } else if (i == error_pos) {
                printf("X");  // 当前误差
            } else {
                printf("-");
            }
        }
        printf("] %.1fms\n", error);
    }
};

通过这些具体示例,可以清楚地看到:

  1. PTS/DTS如何产生和转换
  2. 音视频同步的具体算法实现
  3. B帧对解码顺序的影响
  4. 实际问题的解决方案
  5. 调试和监控方法
相关推荐
办公知识分享小能手2 小时前
怎么把mp4转为mp3呢?分享五个视频转换软件
音视频
一点晖光2 小时前
ffmpeg处理视频命令整理
ffmpeg·音视频
500843 小时前
鸿蒙 Flutter 分布式数据同步:DistributedData 实时协同实战
分布式·flutter·华为·electron·开源·wpf·音视频
Everbrilliant893 小时前
FFmpeg解码视频数据OpenGL Texture播放
ffmpeg·音视频·音视频解码·ffmpeg解码·opengl播放视频·ffmpeg解码gl播放·opengl render
_Cherry|3 小时前
Unity中使用AVPRO播放视频
unity·c#·音视频
wuk9984 小时前
基于帧差分法的视频运动检测MATLAB实现
开发语言·matlab·音视频
Yutengii17 小时前
b站视频下载到电脑本地的方法有哪些
音视频
summerkissyou198719 小时前
Android13-Audio-AudioTrack-播放流程
android·音视频
Black蜡笔小新20 小时前
安防监控/录像存储EasyCVR视频汇聚平台无法启动的原因排查
音视频
xingqing87y21 小时前
祝寿视频怎么制作:4步制作创意祝寿视频
音视频