FFmepg-- 32-ffplay源码- PacketQueue 的线程安全机制 以及 serial 字段的作用

文章目录

      • [一 PacketQueue 的线程安全设计](#一 PacketQueue 的线程安全设计)
      • 线程同步手段
      • [二 serial 字段的作用详解](#二 serial 字段的作用详解)
        • [为什么需要 serial?](#为什么需要 serial?)
        • [serial 的工作机制](#serial 的工作机制)
      • [三 简化版示例代码](#三 简化版示例代码)
      • [四 总结](#四 总结)

一 PacketQueue 的线程安全设计

在 ffplay.c 中,PacketQueue 是一个典型的生产者-消费者队列:

生产者:read_thread(从文件/网络读取 AVPacket 并放入队列)

消费者:解码线程(如 audio_thread / video_thread)从队列中取出 AVPacket 解码

线程同步手段

c 复制代码
typedef struct PacketQueue {
    AVPacketList *first_pkt, *last_pkt;
    int nb_packets;      // 当前包数量
    int size;            // 总字节数(用于限流)
    int64_t duration;    // 总时长(毫秒)
    int abort_request;   // 是否请求终止
    int serial;          // 👈 关键字段:播放序列号
    SDL_mutex *mutex;    // 互斥锁
    SDL_cond *cond;      // 条件变量
} PacketQueue;

SDL_mutex:保护对队列结构体(如 first_pkt, last_pkt, nb_packets 等)的并发访问。

SDL_cond:用于阻塞/唤醒:

消费者调用 packet_queue_get(..., block=1) 时,若队列为空,则 SDL_CondWait(cond, mutex) 阻塞;

生产者调用 packet_queue_put() 后,调用 SDL_CondSignal(cond) 唤醒等待的消费者。

二 serial 字段的作用详解

为什么需要 serial?

当用户执行 seek(快进/快退)操作时,旧的 AVPacket 已经无效,必须被丢弃。此时可能出现以下情况:

read_thread 可能还在往队列里塞旧数据;

解码线程可能还在处理旧数据;

如果不清除这些"过期"数据,就会出现:

音频"回放杂音"

视频跳帧混乱

音画不同步

serial 用于标识"当前播放上下文"的版本号。

serial 的工作机制

初始化时:q->serial = 0

执行 seek 时:

调用 packet_queue_flush(&is->audioq) 清空队列;

插入一个特殊的 flush_pkt(其 data == NULL);

执行 q->serial++(例如从 0 → 1)

此后所有新入队的 packet 都会设置 pkt->serial = q->serial

解码线程在取到 packet 后,会检查:

c 复制代码
if (pkt->serial != decoder->pkt_serial) {
    av_packet_unref(pkt);
    continue; // 丢弃旧序列数据
}

其中 decoder->pkt_serial 会在 flush 后被更新为新的 serial。

三 简化版示例代码

以下是一个高度简化但功能完整的 PacketQueue 实现,突出 serial 和线程安全逻辑:

c 复制代码
#include <SDL2/SDL.h>
#include <libavcodec/avcodec.h>

#define MAX_QUEUE_SIZE (15 * 1024 * 1024)

typedef struct MyAVPacketList {
    AVPacket pkt;
    int serial;
    struct MyAVPacketList *next;
} MyAVPacketList;

typedef struct PacketQueue {
    MyAVPacketList *first, *last;
    int nb_packets;
    int size;
    int64_t duration;
    int abort_request;
    int serial;              // 序列号
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;

void packet_queue_init(PacketQueue *q) {
    memset(q, 0, sizeof(PacketQueue));
    q->mutex = SDL_CreateMutex();
    q->cond = SDL_CreateCond();
    q->serial = 0;
}

int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
    MyAVPacketList *pkt1;

    SDL_LockMutex(q->mutex);

    if (q->abort_request) {
        SDL_UnlockMutex(q->mutex);
        return -1;
    }

    pkt1 = av_malloc(sizeof(MyAVPacketList));
    if (!pkt1) goto fail;

    pkt1->pkt = *pkt;
    pkt1->serial = q->serial;  // 绑定当前 serial
    pkt1->next = NULL;

    if (!q->last)
        q->first = pkt1;
    else
        q->last->next = pkt1;
    q->last = pkt1;

    q->nb_packets++;
    q->size += pkt1->pkt.size + sizeof(*pkt1);
    q->duration += pkt1->pkt.duration;

    SDL_CondSignal(q->cond);  // 唤醒消费者
    SDL_UnlockMutex(q->mutex);
    return 0;

fail:
    SDL_UnlockMutex(q->mutex);
    return -1;
}

// 获取 packet,block=1 表示阻塞等待
int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial) {
    MyAVPacketList *pkt1;
    int ret;

    SDL_LockMutex(q->mutex);

    for (;;) {
        if (q->abort_request) {
            ret = -1;
            break;
        }

        pkt1 = q->first;
        if (pkt1) {
            q->first = pkt1->next;
            if (!q->first)
                q->last = NULL;
            q->nb_packets--;
            q->size -= pkt1->pkt.size + sizeof(*pkt1);
            q->duration -= pkt1->pkt.duration;
            *pkt = pkt1->pkt;
            if (serial)
                *serial = pkt1->serial;  // 返回 packet 的 serial
            av_free(pkt1);
            ret = 1;
            break;
        } else if (!block) {
            ret = 0;
            break;
        } else {
            SDL_CondWait(q->cond, q->mutex);  // 阻塞等待
        }
    }

    SDL_UnlockMutex(q->mutex);
    return ret;
}

// seek 时调用:清空队列 + serial++
void packet_queue_flush(PacketQueue *q) {
    MyAVPacketList *pkt, *pkt1;

    SDL_LockMutex(q->mutex);
    for (pkt = q->first; pkt; pkt = pkt1) {
        pkt1 = pkt->next;
        av_packet_unref(&pkt->pkt);
        av_free(pkt);
    }
    q->first = q->last = NULL;
    q->nb_packets = 0;
    q->size = 0;
    q->duration = 0;
    q->serial++;  // 关键:序列号递增
    SDL_UnlockMutex(q->mutex);
}
使用场景(解码线程伪代码)
c 复制代码
int wanted_serial = is->audioq.serial;  // 期望的 serial

while (1) {
    int serial;
    AVPacket pkt;
    if (packet_queue_get(&is->audioq, &pkt, 1, &serial) < 0)
        break;

    if (serial != wanted_serial) {
        av_packet_unref(&pkt);
        continue;  // 丢弃旧序列数据
    }

    // 正常解码...
    decode_audio(&pkt);
    av_packet_unref(&pkt);
}
Seek 发生时
c 复制代码
// 用户 seek 到新位置
packet_queue_flush(&is->audioq);     // serial 自增
packet_queue_flush(&is->videoq);

// read_thread 会重新开始读取,并给新 packet 打上新 serial

四 总结

机制 作用
SDL_mutex + SDL_cond 实现线程安全的生产者-消费者队列
serial 字段 标识"播放上下文",避免 seek 后旧数据污染
相关推荐
JZC_xiaozhong4 小时前
多系统并行的权限治理难题:如何消除“权限孤岛”与安全风险?
安全·数据安全·etl工程师·iam·数据集成与应用集成·多系统权限管理·统一数据集成
北京聚信万通科技有限公司4 小时前
传输协议:AS3
服务器·网络·安全·电子数据交换·as3
凯新生物4 小时前
mPEG-SS-PLGA-DTX:智能药物递送系统
eureka·flink·ffmpeg·etcd
学而知不足~6 小时前
字幕转码杂记
ffmpeg
互亿无线明明7 小时前
国际金融短信:如何为跨境金融业务构建稳定安全的消息通知链路?
java·python·安全·eclipse·django·virtualenv·pygame
白帽子凯哥哥8 小时前
转行网络安全学习计划与报班建议
学习·安全·web安全·网络安全·渗透测试·漏洞挖掘·网安培训
ReaF_star9 小时前
【基线】关于Debian的一些简单安全配置及验证
学习·安全·debian
飞睿科技9 小时前
ESP Audio Effects音频库迎来专业升级,v1.2.0 新增动态控制核心
人工智能·物联网·ffmpeg·智能家居·语音识别·乐鑫科技·esp
kali-Myon9 小时前
快速解决 Docker 环境中无法打开 gdb 调试窗口以及 tmux 中无法滚动页面内容和无法选中复制的问题
运维·安全·docker·容器·gdb·pwn·tmux