ffplay.c源码阅读之暂停、重播、快进、快退实现细节

前言

1、播放器如何实现暂停?

2、暂停之后在从暂停之处开始播放?

3、播放中快进、后退这些操作实现细节?

以上功能是作为播放器最重要也是非常基础的功能,本文就是仔细学习一下ffplay.c是如何实现这些功能的,希望能够学以致用。

播放暂停和重播

  • 自我分析

前面我们知道ffplay.c有拉流、解码、渲染供6个线程(这里假设视频包含音频和字幕流)。暂停意味着只是暂停播放,所以这些线程不会销毁,所以暂停的时候让它们处于休眠状态,这样就节约了cpu资源,同时各种音视频缓冲区也保留着,待重新开始播放时直接从之前的位置开始。

关键变量paused代表是否暂停,当用户按下暂停后会将该变量设置为1,重新开始播放后又会将该变量设置为0

typedef struct VideoState {
    /// 省略。。。。
    int paused;
    int last_paused;
    // 省略。。。。
}

paused 代表了目前是否暂停状态,last_paused代表了上一次是否暂停状态

下面看一下暂停或者重播的实现逻辑

  • 主线程更新paused变量的值
static void stream_toggle_pause(VideoState *is)
{
    if (is->paused) {
        is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
        if (is->read_pause_return != AVERROR(ENOSYS)) {
            is->vidclk.paused = 0;
        }
        set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
    }
    set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
    is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
}

1、当处从播放状态处于暂停状态后,通过代码set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);更新视频时钟
2、更新外部时钟
3、更新变量paused的值

  • 拉流线程的处理逻辑

这里只贴出当暂停或者重新播放的代码逻辑

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

static int read_thread(void *arg)
{
        // 省略代码......
       // 主要用于处理rtsp等等实时流拉流时的逻辑。当重播或者暂停时要分别调用av_read_play()函数或者av_read_pause()函数
        if (is->paused != is->last_paused) {
            is->last_paused = is->paused;
            if (is->paused)
                is->read_pause_return = av_read_pause(ic);
            else
                av_read_play(ic);
        }
        
        // 当用户暂停后,先将paused变量置为1,此时拉流线程并未结束工作,它依然会继续通过av_read_frame()函数读取未压缩数据包放入未压缩数据队列,直到该队列满让线程休眠10ms(如果一直暂停,则一直不停休眠10ms的状态),这样不会让拉流线程空转
        /* if the queue are full, no need to read more */
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }
      
}

1、当用户按下暂停或者重播后,对于实时流rtsp等等,这里要分别调用av_read_play()函数或者av_read_pause()函数 2、当用户暂停后,先将paused变量置为1,此时拉流线程并未结束工作,它依然会继续通过av_read_frame()函数读取未压缩数据包放入未压缩数据队列,直到该队列满让线程休眠10ms(如果一直暂停,则一直不停休眠10ms的状态),这样不会让拉流线程空转

  • 解码线程的处理逻辑

音视频字幕解码线程的处理逻辑基本都一致,这里以视频为例,主要在函数video_thread()(视频)中,如下为省略了无关紧要的代码

static int video_thread(void *arg)
{
// 省略代码.....
for (;;) {
        // 该函数主要用来获取解码器中的解码结果,当返回<0时,代表解码出现不可描述错误或者解码遇到结束标记了(即到了文件末尾了),小于0时则直接关闭解码线程,否则进入下一步
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;
            // 省略代码.......
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            // 到了这一步代表解码成功,则将解码视频帧插入视频FrameQueue队列,该函数工作原理为,当队列满后,该函数让解码线程处于休眠状态。所以对于暂停后,由于不再继续渲染,即视频FrameQueue不消耗视频帧,故队列肯定会满,即一定会处于休眠状态
            ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            av_frame_unref(frame);

        if (ret < 0)
            goto the_end;
    }
 the_end:
    av_frame_free(&frame);
}

1、暂停后,解码线程继续工作。通过get_video_frame()函数持续获取解码结果,当返回<0时,代表解码出现不可描述错误或者到达文件末尾,那么此时直接结束解码线程。否则进入下一步 2、通过queue_picture()函数将解码视频帧插入视频FrameQueue队列,该函数工作原理为,当队列满后,该函数让解码线程处于休眠状态。所以对于暂停后,由于不再继续渲染,即视频FrameQueue不消耗视频帧,故队列肯定会满,即一定会处于休眠状态

  • 渲染线程

渲染视频和字幕在一个线程,处理逻辑差不多,在函数video_refresh()中。音频是在sdl_audio_callback()中。接下来分别看他们的处理逻辑

暂停后视频和字幕的处理逻辑,先看如下代码:

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
    SDL_PumpEvents();
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
        if (remaining_time > 0.0)
            av_usleep((int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(is, &remaining_time);
        SDL_PumpEvents();
    }
}

当暂停播放后,视频渲染线程依然继续工作直到视频队列FrameQueue中没有数据为止,当此队列空了之后,这里的remaining_time>0会成立,即它会一直循环的进行休眠以免cpu浪费。

接下来是音频渲染的处理逻辑

/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    VideoState *is = opaque;
    int audio_size, len1;

    audio_callback_time = av_gettime_relative();
    
    while (len > 0) {
        if (is->audio_buf_index >= is->audio_buf_size) {
           audio_size = audio_decode_frame(is);
           if (audio_size < 0) {
                /* if error, just output silence */
               is->audio_buf = NULL;
               is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
           } else {
               if (is->show_mode != SHOW_MODE_VIDEO)
                   update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
               is->audio_buf_size = audio_size;
           }
           is->audio_buf_index = 0;
        }
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        else {
            memset(stream, 0, len1);
            if (!is->muted && is->audio_buf)
                SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
        }
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }
    is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }
}

当暂停时audio_decode_frame(is)返回-1,那么此时is->audio_buf_size等于512,但is->audio_buf = NULL;又为nil,即这里的while循环会空转知道len<=0 下次音频渲染线程时又是如此逻辑

疑问?这里既然暂停了不直接return?而是依然让while循环继续执行呢?

快进和快退

快进和快退:将当前播放时间指定到某个指定的时间点,这个时间点可以在当前时间之前也可以之后。快进或者快退要解决两个问题:
1、重新从指定的时间戳开始拉流
2、缓冲区的之前的数据要先清空

以上基本就是实现思路了,接下来就是ffplay.c如何实现了,首先是快进或者快退事件检测

            case SDLK_LEFT:
                incr = seek_interval ? -seek_interval : -10.0;
                goto do_seek;
            case SDLK_RIGHT:
                incr = seek_interval ? seek_interval : 10.0;
                goto do_seek;
            case SDLK_UP:
                incr = 60.0;
                goto do_seek;
            case SDLK_DOWN:
                incr = -60.0;
            do_seek:
                    if (seek_by_bytes) {
                        pos = -1;
                        if (pos < 0 && cur_stream->video_stream >= 0)
                            pos = frame_queue_last_pos(&cur_stream->pictq);
                        if (pos < 0 && cur_stream->audio_stream >= 0)
                            pos = frame_queue_last_pos(&cur_stream->sampq);
                        if (pos < 0)
                            pos = avio_tell(cur_stream->ic->pb);
                        if (cur_stream->ic->bit_rate)
                            incr *= cur_stream->ic->bit_rate / 8.0;
                        else
                            incr *= 180000.0;
                        pos += incr;
                        stream_seek(cur_stream, pos, incr, 1);
                    } else {
                        pos = get_master_clock(cur_stream);
                        if (isnan(pos))
                            pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
                        pos += incr;
                        if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
                            pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
                        stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
                    }
                break;

可以看到上面逻辑非常清晰,当快进或者快退时首先获取当前主时钟的时间,然后基于该时间得到最终的时间点pos。然后通过stream_seek()函数从指定的时间点获取数据

/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
    if (!is->seek_req) {
        is->seek_pos = pos;
        is->seek_rel = rel;
        is->seek_flags &= ~AVSEEK_FLAG_BYTE;
        if (seek_by_bytes)
            is->seek_flags |= AVSEEK_FLAG_BYTE;
        is->seek_req = 1;
        SDL_CondSignal(is->continue_read_thread);
    }
}

seek_req:为1代表当前处于快进或者快退状态
seek_pos:代表快进或者快退到的时间点
seek_rel:代表快进或者快退时间点到当前时间点的时间差
seek_flags:快进搜索的方式(ogg格式支持按字节搜索)

当seek_req = 1后,代表当前处于快进快退状态,当拉流线程检测到目前处于快进或者快退状态则会做相应的处理,具体代码如下:

static int read_thread(void *arg)
{
  //省略代码...
  if (is->seek_req) {
            int64_t seek_target = is->seek_pos;
            int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
            int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
//      of the seek_pos/seek_rel variables

            ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR,
                       "%s: error while seeking\n", is->ic->url);
            } else {
                if (is->audio_stream >= 0) {
                    packet_queue_flush(&is->audioq);
                    packet_queue_put(&is->audioq, &flush_pkt);
                }
                if (is->subtitle_stream >= 0) {
                    packet_queue_flush(&is->subtitleq);
                    packet_queue_put(&is->subtitleq, &flush_pkt);
                }
                if (is->video_stream >= 0) {
                    packet_queue_flush(&is->videoq);
                    packet_queue_put(&is->videoq, &flush_pkt);
                }
                if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                   set_clock(&is->extclk, NAN, 0);
                } else {
                   set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
                }
            }
            is->seek_req = 0;
            is->queue_attachments_req = 1;
            is->eof = 0;
            if (is->paused)
                step_to_next_frame(is);
        }
        if (is->queue_attachments_req) {
            if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
                AVPacket copy = { 0 };
                if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)
                    goto fail;
                packet_queue_put(&is->videoq, &copy);
                packet_queue_put_nullpacket(&is->videoq, is->video_stream);
            }
            is->queue_attachments_req = 0;
        }
  // 省略代码....
}

这段代码主要做了两件事:
1、通过ffmpeg的avformat_seek_file()函数将读取指针移动到指定时间点,接下来的拉流都将从这个时间点之后读取数据(快进和快退操作不支持实时流)
2、将音视频字幕压缩数据PacketQueue清空,并放置一个空数据包,之所以要放置这个空数据包是为了清空解码器
3、同步外部时钟,便于音视频同步

以上就是快进和快退的处理逻辑,可以看到还是比较简单和清晰的

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值