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


    前言

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

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

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

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

    播放暂停和重播

    • 自我分析

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

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

    1. typedef struct VideoState {
    2. /// 省略。。。。
    3. int paused;
    4. int last_paused;
    5. // 省略。。。。
    6. }

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

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

    • 主线程更新paused变量的值
    1. static void stream_toggle_pause(VideoState *is)
    2. {
    3. if (is->paused) {
    4. is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
    5. if (is->read_pause_return != AVERROR(ENOSYS)) {
    6. is->vidclk.paused = 0;
    7. }
    8. set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
    9. }
    10. set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
    11. is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
    12. }

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

    • 拉流线程的处理逻辑

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

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

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

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

    • 解码线程的处理逻辑

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

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

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

    • 渲染线程

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

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

    1. static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    2. double remaining_time = 0.0;
    3. SDL_PumpEvents();
    4. while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
    5. if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
    6. SDL_ShowCursor(0);
    7. cursor_hidden = 1;
    8. }
    9. if (remaining_time > 0.0)
    10. av_usleep((int64_t)(remaining_time * 1000000.0));
    11. remaining_time = REFRESH_RATE;
    12. if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
    13. video_refresh(is, &remaining_time);
    14. SDL_PumpEvents();
    15. }
    16. }

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

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

    1. /* prepare a new audio buffer */
    2. static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
    3. {
    4. VideoState *is = opaque;
    5. int audio_size, len1;
    6. audio_callback_time = av_gettime_relative();
    7. while (len > 0) {
    8. if (is->audio_buf_index >= is->audio_buf_size) {
    9. audio_size = audio_decode_frame(is);
    10. if (audio_size < 0) {
    11. /* if error, just output silence */
    12. is->audio_buf = NULL;
    13. is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
    14. } else {
    15. if (is->show_mode != SHOW_MODE_VIDEO)
    16. update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
    17. is->audio_buf_size = audio_size;
    18. }
    19. is->audio_buf_index = 0;
    20. }
    21. len1 = is->audio_buf_size - is->audio_buf_index;
    22. if (len1 > len)
    23. len1 = len;
    24. if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
    25. memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    26. else {
    27. memset(stream, 0, len1);
    28. if (!is->muted && is->audio_buf)
    29. SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
    30. }
    31. len -= len1;
    32. stream += len1;
    33. is->audio_buf_index += len1;
    34. }
    35. is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    36. /* Let's assume the audio driver that is used by SDL has two periods. */
    37. if (!isnan(is->audio_clock)) {
    38. 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);
    39. sync_clock_to_slave(&is->extclk, &is->audclk);
    40. }
    41. }

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

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

    快进和快退

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

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

    1. case SDLK_LEFT:
    2. incr = seek_interval ? -seek_interval : -10.0;
    3. goto do_seek;
    4. case SDLK_RIGHT:
    5. incr = seek_interval ? seek_interval : 10.0;
    6. goto do_seek;
    7. case SDLK_UP:
    8. incr = 60.0;
    9. goto do_seek;
    10. case SDLK_DOWN:
    11. incr = -60.0;
    12. do_seek:
    13. if (seek_by_bytes) {
    14. pos = -1;
    15. if (pos < 0 && cur_stream->video_stream >= 0)
    16. pos = frame_queue_last_pos(&cur_stream->pictq);
    17. if (pos < 0 && cur_stream->audio_stream >= 0)
    18. pos = frame_queue_last_pos(&cur_stream->sampq);
    19. if (pos < 0)
    20. pos = avio_tell(cur_stream->ic->pb);
    21. if (cur_stream->ic->bit_rate)
    22. incr *= cur_stream->ic->bit_rate / 8.0;
    23. else
    24. incr *= 180000.0;
    25. pos += incr;
    26. stream_seek(cur_stream, pos, incr, 1);
    27. } else {
    28. pos = get_master_clock(cur_stream);
    29. if (isnan(pos))
    30. pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
    31. pos += incr;
    32. if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
    33. pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
    34. stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
    35. }
    36. break;

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

    1. /* seek in the stream */
    2. static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
    3. {
    4. if (!is->seek_req) {
    5. is->seek_pos = pos;
    6. is->seek_rel = rel;
    7. is->seek_flags &= ~AVSEEK_FLAG_BYTE;
    8. if (seek_by_bytes)
    9. is->seek_flags |= AVSEEK_FLAG_BYTE;
    10. is->seek_req = 1;
    11. SDL_CondSignal(is->continue_read_thread);
    12. }
    13. }

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

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

    1. static int read_thread(void *arg)
    2. {
    3. //省略代码...
    4. if (is->seek_req) {
    5. int64_t seek_target = is->seek_pos;
    6. int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
    7. int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
    8. // FIXME the +-2 is due to rounding being not done in the correct direction in generation
    9. // of the seek_pos/seek_rel variables
    10. ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
    11. if (ret < 0) {
    12. av_log(NULL, AV_LOG_ERROR,
    13. "%s: error while seeking\n", is->ic->url);
    14. } else {
    15. if (is->audio_stream >= 0) {
    16. packet_queue_flush(&is->audioq);
    17. packet_queue_put(&is->audioq, &flush_pkt);
    18. }
    19. if (is->subtitle_stream >= 0) {
    20. packet_queue_flush(&is->subtitleq);
    21. packet_queue_put(&is->subtitleq, &flush_pkt);
    22. }
    23. if (is->video_stream >= 0) {
    24. packet_queue_flush(&is->videoq);
    25. packet_queue_put(&is->videoq, &flush_pkt);
    26. }
    27. if (is->seek_flags & AVSEEK_FLAG_BYTE) {
    28. set_clock(&is->extclk, NAN, 0);
    29. } else {
    30. set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
    31. }
    32. }
    33. is->seek_req = 0;
    34. is->queue_attachments_req = 1;
    35. is->eof = 0;
    36. if (is->paused)
    37. step_to_next_frame(is);
    38. }
    39. if (is->queue_attachments_req) {
    40. if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
    41. AVPacket copy = { 0 };
    42. if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)
    43. goto fail;
    44. packet_queue_put(&is->videoq, &copy);
    45. packet_queue_put_nullpacket(&is->videoq, is->video_stream);
    46. }
    47. is->queue_attachments_req = 0;
    48. }
    49. // 省略代码....
    50. }

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

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

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

  • 相关阅读:
    分开的两个程序使用共同的mysql,一端更新了表,另一端怎么及时更新缓存,使用mybatis
    李迟2022年7月工作生活总结
    Git 精简快速使用
    学习记忆——宫殿篇——记忆宫殿——地点桩——演讲稿定位记忆
    第三章-Mybatis源码解析-以xml方式走流程-mapper解析(三)
    用户画像知识点补充——多数据源
    AI 时代的企业级安全合规策略
    用Go实现网络流量解析和行为检测引擎
    安捷伦E9321A射频传感器
    数据可视化高级技术(Echarts)
  • 原文地址:https://blog.csdn.net/m0_60259116/article/details/126527042