ijkplayer的序列号(serial)分析

serial在ijkplayer中主要用于在seek操作后区分连续的数据流,它存在于PacketQueue、MyAVPacketList、Decoder、Frame、SDL_AMediaCodec和Clock等结构体中。serial的改变通常是因为flush包的put,特别是在stream_component_open、packet_queue_start和seek操作后。Decoder的serial记录上次解码的AVPacket序列号,用于判断是否需要解码新数据。Frame的serial影响视频帧的显示和时间计算。SDL_AMediaCodec的object_serial在硬解码时涉及,与渲染流程的同步有关。Clock的serial用于音视频同步和丢帧处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


ijkplayer 中很多地方都用到了序列号 (serial) 相关的字段,为什么会需要这个字段呢?其实这是为 seek 这个功能作准备的。因为 seek 之后,之前缓冲好的数据就不能用了,seek 前后的数据是“不连续的”,我们用 serial 这个字段来区分是否是连续的数据。
serial 概念主要存在于以下几个结构体中:

  1. struct PacketQueue
typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;
    ...
    int serial;
    MyAVPacketList *recycle_pkt;
    ...
} PacketQueue;
  1. struct MyAVPacketList
typedef struct MyAVPacketList {
    AVPacket pkt;
    struct MyAVPacketList *next;
    int serial;
} MyAVPacketList;
  1. struct Decoder 中的 pkt_serial
typedef struct Decoder {
    AVPacket pkt;
    PacketQueue *queue;
    ...
    int pkt_serial;
    int finished;
    int packet_pending;
    ...
} Decoder;
  1. struct Frame
typedef struct Frame {
    AVFrame *frame;
    ...
    int serial;
    double pts;           /* presentation timestamp for the frame */
    double duration;      /* estimated duration of the frame */
    ...
} Frame;
  1. struct SDL_AMediaCodec 中的 object_serial
typedef struct SDL_AMediaCodec
{
    ...
    SDL_AMediaCodec_Opaque *opaque;
    int                     object_serial;
    ...
} SDL_AMediaCodec;

6.typedef struct Clock 中的 serial 和 queue_serial

typedef struct Clock {
    double pts;           /* clock base */
	...
    int serial;           /* clock is based on a packet with this serial */
    int paused;
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

接下来我们来具体分析下 serial 的作用。

首先,可以先抛出一个结论:serial 的连环改变都是因为往 packetqueue 中放入一个 flush 包引起的。我们可以先来分析下 PacketQueue 和 MyAVPacketList 中的序列号。

1. PacketQueue 和 MyAVPacketList 中的序列号

PacketQueue 的 serial 是在 packet_queue_put_private 中发生改变的。

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;
	//......
    pkt1->pkt = *pkt;
    pkt1->next = NULL;
    if (pkt == &flush_pkt) //如果put了一个flush_pkt,队列的序列号会增加
        q->serial++;
    pkt1->serial = q->serial;//MyAVPacketList的serial也会跟PacketQueue保持一样 
    //......
    return 0;
}

那么什么时候会 put 一个 flush_pkt ?

Put flush_pkt 的时机

  1. stream_component_open -> packet_queue_start -> packet_queue_put_private(q, &flush_pkt)
    在一开始打开一个流的时候,启动队列时会 put 进一个flush_pkt。
static void packet_queue_start(PacketQueue *q)
{
    SDL_LockMutex(q->mutex);
    q->abort_request = 0;
    packet_queue_put_private(q, &flush_pkt);
    SDL_UnlockMutex(q->mutex);
}

这时候 serial 发生初次更改,serial = 1。

  1. 在成功完成 seek 后,会往包队列中 put 进一个flush_pkt
    在这里插入图片描述
    每发生一次 seek ,serial 都会累加一次。

2. Decoder 中的 pkt_serial

经由 get_video_frame -> decoder_decode_frame -> packet_queue_get_or_buffering 中,会对 Decoder 的 pkt_serial 进行赋值。

do {
	if (d->queue->nb_packets == 0)
	    SDL_CondSignal(d->empty_queue_cond);
	if (d->packet_pending) {
	    av_packet_move_ref(&pkt, &d->pkt);
	    d->packet_pending = 0;
	} else {
		// &d->pkt_serial 传入 packet_queue_get_or_buffering,可以在函数中获得值
	    if (packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0)
	        return -1;
	}
} while (d->queue->serial != d->pkt_serial);//比较是否是连续的流
static int packet_queue_get_or_buffering(FFPlayer *ffp, PacketQueue *q, AVPacket *pkt, int *serial, int *finished)
{
    assert(finished);
    if (!ffp->packet_buffering)
        return packet_queue_get(q, pkt, 1, serial);

    while (1) {
    	// 再传入 packet_queue_get 中
        int new_packet = packet_queue_get(q, pkt, 0, serial);
        //......
        if (*finished == *serial) {
            av_packet_unref(pkt);
            continue;
        }
        else
            break;
    }
    return 1;
}
/* return < 0 if aborted, 0 if no packet and > 0 if packet.  */
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList *pkt1;
    int ret;
    //......
    pkt1 = q->first_pkt;
    if (pkt1) {
        q->first_pkt = pkt1->next;
        if (!q->first_pkt)
            q->last_pkt = NULL;
        q->nb_packets--;
        q->size -= pkt1->pkt.size + sizeof(*pkt1);
        q->duration -= FFMAX(pkt1->pkt.duration, MIN_PKT_DURATION);
        *pkt = pkt1->pkt;
        if (serial)
            *serial = pkt1->serial;//在此处MyAVPacketList的serial赋予给了Deocder的pkt_serial
	//......
    return ret;
}

可以说,Decoder 的 serial 记录了上一次解码所用的 AVPacket 的序列号。
那么,Decoder 的 serial 主要在什么地方会用到?如下:

  1. 上面图中 packet_queue_get_or_buffering 所在的 do-while 循环判断条件,判断是否连续的流,这里获得的 packet 需要是最新的 serial 的
  2. 在解码函数中会先比对 PacketQueue 的 serial 和 Decoder 的 serial 是否相等,不相等的话就不需要再进行解码工作了,放弃解码器旧的无效缓存
    在这里插入图片描述
  3. audio_thread 中将解码所得的 AVFrame 放入帧队列前,为 Frame 的 serial 赋值
    af->serial = is->auddec.pkt_serial;
    
  4. decoder_decode_frame 中在解码完成时,给 Decoder.finished 赋值
    if (ret == AVERROR_EOF) {
        d->finished = d->pkt_serial;
        avcodec_flush_buffers(d->avctx);
        return 0;
    }
    
    Decoder.finished 用于判断是否播放完成

3. Frame 中的 serial

Frame 的 serial 在帧入队列前被 Deocder.pkt_serial 赋值,如前面第 3 小点讲到。
Frame 的 serial 作用体现在:

  1. 视频显示 video_refresh 函数中
    if (lastvp->serial != vp->serial)
                    is->frame_timer = av_gettime_relative() / 1000000.0;
    
    frame_timer 代表了视频帧当前真实的时间轴,当出现“不连续的情况” ,则重新更新 frame_timer
  2. video_refresh 中
    if (vp->serial != is->videoq.serial) {
        frame_queue_next(&is->pictq);
        goto retry;
    }
    
    与 PacketQueue 的 serial 不相等,则重新去获取下一个视频 frame
  3. video_refresh 中
    static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
    if (vp->serial == nextvp->serial) {
        double duration = nextvp->pts - vp->pts;
        if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
            return vp->duration;
        else
            return duration;
        } else {
            return 0.0;
        }
    }
    
    vp_duration 用于计算相邻两帧之间的 pts 时间差,当相邻两帧同流的情况下才进行计算
  4. audio_decode_frame 中获取音频 frame
    do {
        if (!(af = frame_queue_peek_readable(&is->sampq)))
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);
    

4. SDL_AMediaCodec 中的 object_serial

当调用 MediaCodec 硬解时才会用到
首次赋值是在创建 Codec 时发生的,在 SDL_AMediaCodecJava_createByCodecName -> SDL_AMediaCodec_create_object_serial 函数中:

int SDL_AMediaCodec_create_object_serial()
{
    int object_serial = __sync_add_and_fetch(&g_amediacodec_object_serial, 1);
    if (object_serial == 0)
        object_serial = __sync_add_and_fetch(&g_amediacodec_object_serial, 1);
    return object_serial;
}

g_amediacodec_object_serial 是一个静态的全局变量,初始值为 0,所以一般来说上面这个函数返回 1 给 SDL_AMediaCodec.object_serial,即 SDL_AMediaCodec.object_serial 变为 1
而当 Codec flush 刷新时,则会得到更新:
feed_input_buffer->SDL_AMediaCodec_flush->SDL_AMediaCodecJava_flush->SDL_AMediaCodec_create_object_serial,再一次调用 SDL_AMediaCodec_create_object_serial 此函数,使得 SDL_AMediaCodec.object_serial 加1
Codec flush 刷新,是在以下场景发生的,看下面这段代码:
在这里插入图片描述
注意 ffp_is_flush_packet(&pkt) 和 opaque->acodec_flush_request 这两个条件。一般来说,当进行 seek 后,将会清空队列并扔进一个 flush 包,也即 seek 后会进入 SDL_AMediaCodec_flush 的逻辑当中
然后,还需要结合另一个结构体字段来看,SDL_AMediaCodecBufferProxy 中的 acodec_serial

struct SDL_AMediaCodecBufferProxy
{
    int buffer_id;
    int buffer_index;
    int acodec_serial;
    SDL_AMediaCodecBufferInfo buffer_info;
};

在 drain_output_buffer_l -> amc_fill_frame -> SDL_VoutAndroid_obtainBufferProxy -> SDL_VoutAndroid_obtainBufferProxy_l 里 SDL_AMediaCodecBufferProxy.acodec_serial 会被 SDL_AMediaCodec.object_serial 赋值,相当于 SDL_AMediaCodecBufferProxy.acodec_serial 相当于保存了解码后 SDL_AMediaCodec.object_serial 的值
SDL_AMediaCodec_flush 触发 SDL_AMediaCodec.object_serial 改变后,在渲染时 SDL_VoutAndroid_releaseBufferProxy_l 函数会进行一次判断 SDL_AMediaCodec_isSameSerial,判断上面这两个字段的值是否相等

bool SDL_AMediaCodec_isSameSerial(SDL_AMediaCodec* acodec, int acodec_serial)
{
    if (acodec == NULL)
        return false;
    return acodec->object_serial == acodec_serial;
}

不相等则函数返回,不进行后面的渲染流程

5. Clock 中的 serial

Clock serial 主要是在音视频帧在要被播放的时候更新的

  1. Audio 部分: 在取音频帧的函数 audio_decode_frame 中,赋值 is->audio_clock_serial,而 is->audio_clock_serial 又会在 sdl_audio_callback 回调函数中的 set_clock_at 里被赋值给 Clock.serial

    is->audio_clock_serial = af->serial;
    
    if (!isnan(is->audio_clock)) {
    	set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);
    	sync_clock_to_slave(&is->extclk, &is->audclk);
    }
    
  2. Video 部分: 在视频显示函数 video_refresh 中,使用 Frame.serial 进行赋值

    update_video_pts(is, vp->pts, vp->pos, vp->serial);
    
  3. *Clock.queue_serial, 在初始化时钟时用 PacketQueue.serial 的地址进行赋值

    init_clock(&is->vidclk, &is->videoq.serial);
    init_clock(&is->audclk, &is->audioq.serial);
    

用途:

  1. get_video_frame 解码函数中, 作丢帧处理时判断是否与 Decoder.pkt_serial 相等
    if (ffp->framedrop>0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
                ffp->stat.decode_frame_count++;
                if (frame->pts != AV_NOPTS_VALUE) {
                    double diff = dpts - get_master_clock(is);
                    if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
                        diff - is->frame_last_filter_delay < 0 &&
                        is->viddec.pkt_serial == is->vidclk.serial &&
                        is->videoq.nb_packets) {
                        is->frame_drops_early++;
                        //...
    
  2. 在 get_clock 时可以判断是否连续
    static double get_clock(Clock *c)
    {
        if (*c->queue_serial != c->serial)//先判断与queue_serial是否相等,其实相当于比较是否与is->videoq.serial或is->audio.serial相等
            return NAN;
        if (c->paused) {
            return c->pts;
        } else {
            double time = av_gettime_relative() / 1000000.0;
            return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
        }
    }
    
ijkplayer 的动态库v7a 和v8a,支持海康摄像头RTSP h264协议,源码来自最新版本ijkplayer k0.8.0 使用原DEMO即可支持RTSP mp4文件等播放,首画面500ms显示. module-lite-rtsp.sh的内容如下: #! /usr/bin/env bash #-------------------- # Standard options: export COMMON_FF_CFG_FLAGS= # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --prefix=PREFIX" # Licensing options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-gpl" # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-version3" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-nonfree" # Configuration options: # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-static" # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-shared" # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-small" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-runtime-cpudetect" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-gray" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-swscale-alpha" # Program options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-programs" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffmpeg" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffplay" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffprobe" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffserver" # Documentation options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-doc" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-htmlpages" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-manpages" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-podpages" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-txtpages" # Component options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-avdevice" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-avcodec" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-avformat" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-avutil" export COMMON_FF_CF
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值