音视频同步版本【基于外部时钟】--版本的优化,如今视频可以正常至播放竣事

[复制链接]
发表于 2026-1-3 10:46:10 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
基于 音视频同步版本【基于外部时钟】 版本的优化,之前的每个代码版本都没有对获取到的包和帧举行内存开释,导致内存占用过多无法创建新的包瓦解了。这个版本:

  • 创建平凡变量packet对象,而不是指针packet了【如许方便调用av_packet_unref()。假如利用指针就须要在每次while的开头重新new一次,然后末了在av_packet_free()一次,比力贫困。】
  • 在利用了包的地方加上了av_packet_unref()
  • 在利用帧的地方加上了av_free_free()。
很希奇!!!!
标题:在主线程的while读取包和帧的地方,第一句的printf偶然间会导致步伐瓦解。表明掉了就不会有标题,但是假如放开,那么偶然间启动就会瓦解,偶然间就不会瓦解。

  • 多线程标题:
    假如步伐是多线程的,而且printf被多个线程同时调用,而没有恰当的同步机制,则大概会导致数据竞争和瓦解。
    答案:大概是如许?待确认
  1. #include <iostream>
  2. #include <windows.h>
  3. #include<queue>
  4. #include<chrono>
  5. #include<ctime>
  6. #ifdef __cplusplus  ///
  7. extern "C"
  8. {
  9. // 包含ffmpeg头文件
  10. #include "libavutil/avutil.h"
  11. #include"libavformat/avformat.h"
  12. #include"libswscale/swscale.h"
  13. #include"libswresample/swresample.h"
  14. // 包含SDL头文件
  15. #include"SDL.h"
  16. }
  17. #endif
  18. using namespace std;
  19. class AVSync{
  20. public:
  21.     AVSync() {
  22.     }
  23.     void init()
  24.     {
  25.         start_time = getNowMilliseconds();
  26.     }
  27.     // 获取当前时间的pts应该是多少了
  28.     int getPts() {
  29.         return getNowMilliseconds() - start_time;
  30.     }
  31.     // 毫秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876】
  32.     Uint64 getNowMilliseconds() {
  33.         return getNowMicroseconds() / 1000;
  34.     }
  35.     // 微秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876543】
  36.     Uint64 getNowMicroseconds() {
  37.         using namespace std::chrono;
  38.         //
  39.         system_clock::time_point time_point_now = system_clock::now();
  40.         system_clock::duration duration = time_point_now.time_since_epoch();
  41.         return duration_cast<microseconds>(duration).count();
  42.     }
  43. private:
  44.     // 音视频播放启动的时间--毫秒时间戳
  45.     Uint64 start_time = 0;
  46. };
  47. // 线程停止运行标识,0为正在运行,1为停止
  48. int thread_exit = 0;
  49. // 当前帧音频PCM数据
  50. static Uint8 *audio_pcm_g;
  51. // 当前帧音频PCM数据的字节总大小长度
  52. static Uint32 audio_len_g;
  53. // 音视频帧队列
  54. queue<AVFrame*> audio_frame_queue_;
  55. queue<AVFrame*> video_frame_queue_;
  56. // 将main方法中的变量提取到全局以供两个线程函数中使用
  57. AVFormatContext *input_fmt_ctx = NULL;
  58. int video_idx = -1;
  59. int audio_idx = -1;
  60. AVCodecContext *audio_codec_ctx;
  61. // 音视频同步工具类
  62. AVSync sync_;
  63. // 输出错误信息
  64. void showError(int ret, const char *methodName = "method")
  65. {
  66.     if(ret == 0) {
  67.         return ;
  68.     }
  69.     // 错误消息日志日志
  70.     char err2str[256];
  71.     // 将返回结果转化为字符串信息
  72.     av_strerror(ret, err2str, sizeof(err2str));
  73.     printf("%s failed, ret:%d, msg:%s\n", methodName, ret, err2str);
  74. }
  75. // 填充PCM数据到SDL中
  76. void fill_audio_pcm(void *udata, Uint8 *stream, int len) {
  77.     // 清空上一帧的数据
  78.     SDL_memset(stream, 0, len);
  79.     // 如果外部线程【主线程读帧】还未读取到数据,那么无法填充PCM到SDL中进行播放
  80.     if(audio_len_g == 0)
  81.     {
  82.         return ;
  83.     }
  84.     // 本次回调结束最多只能取len字节的数据
  85.     // 如果外部读取的帧小于len字节,那么直接填充外部读取到的所有数据即可
  86.     // 如果外部读取的帧大于len字节,那么本次填充len字节的数据,等下次回调再填充 audio_len_g - len字节的数据
  87.     // 【如果audio_len_g - len 还是大于了len字节,那么继续取len填充即可】
  88.     len = len > audio_len_g ? audio_len_g : len;
  89.     //填充PCM数据到SDL中
  90.     SDL_MixAudio(stream, audio_pcm_g, len, SDL_MIX_MAXVOLUME/2);// SDL_MIX_MAXVOLUME/2 为音频大小,在0-128之间调整
  91.     // 更新pcm内存指针指向位置,已经【又】使用了len个字节空间,那么下次需要从当前位置+len的位置开始使用
  92.     audio_pcm_g += len;
  93.     // 更新剩余字节大小数量,已经读取了len个字节大小的数据,那么下次还剩 audio_len_g - len 个字节大小的数据可以使用
  94.     audio_len_g -= len;
  95. }
  96. // 视频播放线程
  97. int play_video_thread(void *opaque) {
  98.     // SDL
  99.     // 初始化视频
  100.     if(SDL_Init(SDL_INIT_VIDEO)) {
  101.         return -1;
  102.     }
  103.     // 视频宽度
  104.     int video_width_ = input_fmt_ctx->streams[video_idx]->codecpar->width;
  105.     // 视频高度
  106.     int video_height_ = input_fmt_ctx->streams[video_idx]->codecpar->height;
  107.     // 创建窗口--显示器
  108.     // 在这里设置显示出来的窗口的总大小
  109.     SDL_Window *win_ = SDL_CreateWindow("苏花末测试窗口", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
  110.                                     video_width_, video_height_, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
  111.     if(!win_) {
  112.         return -1;
  113.     }
  114.     // 渲染器,用于将纹理渲染到窗口上
  115.     SDL_Renderer *renderer_ = SDL_CreateRenderer(win_, -1, 0);
  116.     if(!renderer_) {
  117.         return -1;
  118.     }
  119.     // 纹理,用于设置渲染图片数据
  120.     SDL_Texture *texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_IYUV,  SDL_TEXTUREACCESS_STREAMING, video_width_, video_height_);
  121.     if(!texture_) {
  122.         return -1;
  123.     }
  124.     // Rect--页面显示区域
  125.     SDL_Rect rect_;
  126.     // 刷新事件队列【防止有缓存】
  127.     SDL_PumpEvents();
  128.     SDL_Event event;
  129.     // 线程运行中
  130.     while(thread_exit == 0)
  131.     {
  132.         // 在没有事件的情况下才能刷新页面
  133.         if(SDL_PollEvent(&event) != 0) {
  134.             continue;
  135.         }
  136.         AVFrame *frame = video_frame_queue_.front();
  137.         // 帧的相对时间过去了多久【单位:1/刻度 秒】
  138.         double pts = frame->pts * av_q2d(input_fmt_ctx->streams[video_idx]->time_base);
  139.         printf("video.pts: %f ; now.pts: %f\n", pts, sync_.getPts() / 1000.0);
  140.         pts = pts * 1000;//转换毫秒
  141.         // 当前帧如果实际上应该播放的时间超过了当前时间,则代表当前帧应该在未来播放,现在不能播放
  142.         // 故在这里等待时间
  143.         if(pts > sync_.getPts())
  144.         {
  145.             SDL_Delay(pts - sync_.getPts());//等待时间直到时间到了pts,那么才播放
  146.             // 这里可以使用continue,或者也可以直接向下运行,加个continue方便盘逻辑
  147.             continue;
  148.         }
  149.         // 将当前帧移除待播放队列,因为这一帧马上就播放了
  150.         video_frame_queue_.pop();
  151.         // 如果当前帧已经延迟了500ms了,那么丢弃该帧,直接播放下一帧
  152.         if(pts < sync_.getPts() - 500)
  153.         {
  154.             continue;
  155.         }
  156.         // SDL: output
  157.         // 清空之前的页面
  158.         SDL_RenderClear(renderer_);
  159.         // 设置rect所占区域
  160.         rect_.x = 0;
  161.         rect_.y = 0;
  162.         // 在这里设置rect区域的大小,如果这里和窗口总大小不一样,那么其他地方是黑屏显示
  163.         // 故这里也体现了一个win可以设置多个rect,每个rect可以占据不同的位置
  164.         rect_.w = video_width_;
  165.         rect_.h = video_height_;
  166.         // 通过YUV格式渲染图片
  167.         SDL_UpdateYUVTexture(texture_, &rect_,
  168.                 frame->data[0], frame->linesize[0],
  169.                 frame->data[1], frame->linesize[1],
  170.                 frame->data[2], frame->linesize[2]);
  171.         // 页面内容设置
  172.         SDL_RenderCopy(renderer_, texture_, NULL, &rect_);
  173.         // 显示新的页面
  174.         SDL_RenderPresent(renderer_);
  175.         // 释放内存
  176.         av_frame_free(&frame);
  177.     }
  178.     return 0;
  179. }
  180. // 音频播放线程
  181. int play_audio_thread(void *opaque) {
  182.     int ret = 0;
  183.     // 初始化音频
  184.     if(SDL_Init(SDL_INIT_AUDIO)) {
  185.         return -1;
  186.     }
  187.     // 音频播放上下文,音频播放只能通过这个结构体进行操作
  188.     // 创建 SwrContext 只能使用 swr_alloc() 函数
  189.     SwrContext *swrContext = swr_alloc();
  190.     if(!swrContext)
  191.     {
  192.         cout << "初始化swrContext对象失败" << endl;
  193.         return -1;
  194.     }
  195.     // 设置具体参数来创建 SwrContext对象
  196.     /* channel布局:如立体声、5.1声道、单声道等
  197.      * 采样格式:不同音频格式的采样格式不同,如AAC的采样格式是 AV_SAMPLE_FMT_FLTP,
  198.      *      而MP3的采样格式是 AV_SAMPLE_FMT_S16P
  199.      * 采样率:一秒钟采集多少次样本
  200.      * */
  201.     swrContext = swr_alloc_set_opts(NULL,   //是否需要继承一个存在的SwrContext的内容
  202.                                     AV_CH_LAYOUT_STEREO, //输出的channel布局
  203.                                     AV_SAMPLE_FMT_S16, //输出的采样格式
  204.                                     44100, //输出的采样率
  205.                                     av_get_default_channel_layout(audio_codec_ctx->channels), //输入的channel布局
  206.                                     audio_codec_ctx->sample_fmt, //输入的采用格式
  207.                                     audio_codec_ctx->sample_rate, //输入的采用率
  208.                                     0,
  209.                                     NULL);
  210.     /* 为什么要重采样?
  211.      * 是因为输入的音频可能是mp4格式的,但是我们的电脑只能播放avi格式的音频,
  212.      *  所以需要转换数据,转换为确保我们的电脑一定能播放的格式。
  213.      * */
  214.     // 初始化重采样上下文
  215.     ret = swr_init(swrContext);
  216.     // 初始化重采样失败,那么音频无法播放
  217.     if(ret < 0)
  218.     {
  219.         cout << "初始化重采样上下文失败" << endl;
  220.         return -1;
  221.     }
  222.     // SDL_AudioSpc 是音频播放参数的结构体
  223.     // 期望能够实现的音频参数
  224.     SDL_AudioSpec wanted_spec;
  225.     wanted_spec.freq = 44100; //期望的采样率
  226.     wanted_spec.format = AUDIO_S16SYS; //期望的采样格式
  227.     wanted_spec.channels = 2; //期望的通道格式
  228.     wanted_spec.silence = 0; //期望中静音大小的值
  229.     wanted_spec.samples = 1024; //期望中一帧的数据大小,即样本数
  230.     wanted_spec.callback = fill_audio_pcm;//播放音频时会开启一个线程,反复调用这个回调函数,用来给音频填充PCM
  231.     wanted_spec.userdata = audio_codec_ctx; //回调函数中第一个参数的对象
  232.     // 按照指定参数打开真实的物理设备
  233.     ret = SDL_OpenAudio(&wanted_spec, NULL);
  234.     if(ret < 0)
  235.     {
  236.         cout << "打开音频设备失败" << endl;
  237.         return -1;
  238.     }
  239.     // 开始播放音频
  240.     SDL_PauseAudio(0);
  241.     // 分配输出音频数据
  242.     Uint8 *out_buffer = nullptr;
  243.     // 线程运行中
  244.     while(thread_exit == 0)
  245.     {
  246.         // 如果队列为空,则等待帧
  247.         if(audio_frame_queue_.empty())
  248.         {
  249.             SDL_Delay(1);
  250.             continue;
  251.         }
  252.         AVFrame *frame = audio_frame_queue_.front();
  253.         // 帧的相对时间过去了多久【单位:1/刻度 秒】
  254.         double pts = frame->pts * av_q2d(input_fmt_ctx->streams[audio_idx]->time_base);
  255.         printf("audio.pts: %f ; now.pts: %f\n", pts, sync_.getPts() / 1000.0);
  256.         pts = pts * 1000;//转换毫秒
  257.         // 当前帧如果实际上应该播放的时间超过了当前时间,则代表当前帧应该在未来播放,现在不能播放
  258.         // 故在这里等待时间
  259.         if(pts > sync_.getPts())
  260.         {
  261.             SDL_Delay(pts - sync_.getPts());//等待时间直到时间到了pts,那么才播放
  262.             // 这里可以使用continue,或者也可以直接向下运行,加个continue方便盘逻辑
  263.             continue;
  264.         }
  265.         // 将当前帧移除待播放队列,因为这一帧马上就播放了
  266.         audio_frame_queue_.pop();
  267.         // 如果当前帧已经延迟了500ms了,那么丢弃该帧,直接播放下一帧
  268.         if(pts < sync_.getPts() - 500)
  269.         {
  270.             continue;
  271.         }
  272.         // 获取输入的样本数
  273.         int in_samples = frame->nb_samples;
  274.         // 目标样本数【想要输出的样本数】
  275.         int dst_samples = av_rescale_rnd(in_samples, wanted_spec.freq, frame->sample_rate, AV_ROUND_UP);
  276.         // 计算需要输出的样本数内存空间大小
  277.         int out_buffer_size = av_samples_get_buffer_size(NULL, wanted_spec.channels,
  278.                                                          dst_samples, AV_SAMPLE_FMT_S16, 0);
  279.         // 如果输出的音频数据未开辟过空间,那么开辟空间
  280.         if(!out_buffer)
  281.         {
  282.             // 输出数据的空间大小即为计算出来需要输出的样本数大小
  283.             out_buffer = (Uint8 *)av_malloc(out_buffer_size);
  284.         }
  285.         // 返回每个通道需要输出的样本数,错误时返回负值
  286.         int sample_count = swr_convert(swrContext, &out_buffer, dst_samples,
  287.                                       (const Uint8 **)frame->data, in_samples);// frame->data 即为采样到的数据
  288.         // 释放内存
  289.         av_frame_free(&frame);
  290.         // 获取不到样本数了,那么进行下一个包数据的读取
  291.         if(sample_count < 0)
  292.         {
  293.             break;
  294.         }
  295.         // 计算这一帧的字节数大小/长度
  296.         int out_size = sample_count * wanted_spec.channels *av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
  297.         // 如果回调函数中的字节数还未处理完,那么不能进行下一个音频帧的处理
  298.         while(audio_len_g > 0)
  299.             SDL_Delay(1);
  300.         // 回调函数中的字节已经处理完了,那么可以填充下一个音频帧需要的数据了
  301.         // 这一帧的字节长度
  302.         audio_len_g = out_size;
  303.         // 填充pcm数据
  304.         audio_pcm_g = (Uint8 *)out_buffer;
  305.     }
  306.     return 0;
  307. }
  308. // 将帧写入队列
  309. void push_frame(queue<AVFrame*> &queue_, AVFrame *frame_) {
  310.     AVFrame *frame = av_frame_alloc();
  311.     av_frame_move_ref(frame, frame_);
  312.     queue_.push(frame);
  313. }
  314. #undef main
  315. int main(int argc, char *argv[])
  316. {
  317.     SetConsoleOutputCP(CP_UTF8);
  318.     if(argc < 2)
  319.     {
  320.         cout << "请输入视频地址" << endl;
  321.         return -1;
  322.     }
  323.     // 获取视频地址
  324.     char *url = argv[1];
  325.     // 方法调用结果
  326.     int ret = 0;
  327.     // FFmpeg
  328.     // AVFormatContext 是音视频开发使用到最多的结构体,无论什么函数基本上都会用到它
  329.     // AVFormatContext 只能通过 avformat_alloc_context() 创建空的对象
  330.     input_fmt_ctx = avformat_alloc_context();
  331.     // 加载视频内容到音视频格式上下文中
  332.     ret = avformat_open_input(&input_fmt_ctx, url, NULL, NULL);
  333.     // 输出日志日志
  334.     showError(ret);
  335.     // 查看流信息,可以不写,只是单纯拿返回值来做校验的
  336.     ret = avformat_find_stream_info(input_fmt_ctx, NULL);
  337.     // 输出日志日志
  338.     showError(ret);
  339.     // 输出视频信息,可以不写
  340.     av_dump_format(input_fmt_ctx, 0, url, 0);
  341.     // 查找指定流的idx,如果使用不到,可以不写; AVMEDIA_TYPE_VIDEO 代表视频流,AVMEDIA_TYPE_AUDIO代表音频流
  342.     video_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
  343.     audio_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
  344.     printf("video_idx: %d , audio_idx: %d\n", video_idx, audio_idx);
  345.     // AVCodecContext 是解码器上下文,需要对帧处理基本上都会用到它
  346.     // AVCodecContext 只能通过 avcodec_alloc_context3(NULL) 创建空的对象
  347.     // 视频解码器上下文
  348.     AVCodecContext *video_codec_ctx = avcodec_alloc_context3(NULL);
  349.     // 将音视频格式上下文中的参数加载到解码器上下文对象中
  350.     ret = avcodec_parameters_to_context(video_codec_ctx, input_fmt_ctx->streams[video_idx]->codecpar);
  351.     // 输出日志
  352.     showError(ret);
  353.     // 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容
  354.     AVCodec *video_codec = avcodec_find_decoder(video_codec_ctx->codec_id);
  355.     // 将物理解码器加载到解码器上下文中
  356.     ret = avcodec_open2(video_codec_ctx, video_codec, NULL);
  357.     // 音频解码器上下文
  358.     audio_codec_ctx = avcodec_alloc_context3(NULL);
  359.     // 将音视频格式上下文中的参数加载到解码器上下文对象中
  360.     ret = avcodec_parameters_to_context(audio_codec_ctx, input_fmt_ctx->streams[audio_idx]->codecpar);
  361.     // 输出日志
  362.     showError(ret);
  363.     // 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容
  364.     AVCodec *audio_codec = avcodec_find_decoder(audio_codec_ctx->codec_id);
  365.     // 将物理解码器加载到解码器上下文中
  366.     ret = avcodec_open2(audio_codec_ctx, audio_codec, NULL);
  367.     // 输出日志
  368.     showError(ret);
  369.     // 包,用来获取音视频格式上下文中的数据
  370.     // AVPacket 只能通过 av_packet_alloc() 创建对象
  371.     AVPacket pkt;
  372.     // 开启播放线程
  373.     SDL_CreateThread(play_video_thread, NULL, NULL);
  374.     SDL_CreateThread(play_audio_thread, NULL, NULL);
  375.     // 设置时钟
  376.     sync_.init();
  377.     // output and readFrame
  378.     while(1)
  379.     {
  380.         // printf("video_queue.size: %d ; audio_queue.size: %d\n", video_frame_queue_.size(), audio_frame_queue_.size());
  381.         // 防止读取内存过大
  382.         if(video_frame_queue_.size() >= 100 || audio_frame_queue_.size() >= 100)
  383.         {
  384.             SDL_Delay(1);
  385.             continue;
  386.         }
  387.         // FFmpeg: readFrame
  388.         // 获取该音视频格式上下文中的第一个包,并将从音视频格式上下文中移除
  389.         // 则代表了每次调用都会获取到新的包,之前的包不会再在该音视频格式上下文中找到了
  390.         ret = av_read_frame(input_fmt_ctx, &pkt);
  391.         // 如果包数据读取完毕,则代表视频播放结束了
  392.         if(ret < 0)
  393.         {
  394.             cout << "play video finish" << endl;
  395.             break;
  396.         }
  397.         // AVFrame 只能通过 av_frame_alloc() 创建对象
  398.         AVFrame *frame = av_frame_alloc();
  399.         // 音频帧
  400.         if(pkt.stream_index == audio_idx)
  401.         {
  402.             // 将包加载到解码器上下文中进行解码
  403.             ret = avcodec_send_packet(audio_codec_ctx, &pkt);
  404.             // 对应音频的包数据来说,一次包读取,可以获取到多个frame
  405.             while(1)
  406.             {
  407.                 // 读取解码后的包中的帧
  408.                 ret = avcodec_receive_frame(audio_codec_ctx, frame);
  409.                 // 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧
  410.                 // 如果所有的帧都读取完成了,那么开始读取下一个包
  411.                 if(ret == AVERROR(EAGAIN))
  412.                 {
  413.                     break;
  414.                 }
  415.                 // 将帧添加到队列中
  416.                 push_frame(audio_frame_queue_, frame);
  417.             }
  418.         }
  419.         // 视频帧
  420.         else if(pkt.stream_index == video_idx)
  421.         {
  422.             // 将包加载到解码器上下文中进行解码
  423.             ret = avcodec_send_packet(video_codec_ctx, &pkt);
  424.             // 读取解码后的包中的帧
  425.             ret = avcodec_receive_frame(video_codec_ctx, frame);
  426.             // 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧
  427.             if(AVERROR(EAGAIN) == ret)
  428.             {
  429.                 continue;
  430.             }
  431.             // 将帧添加到队列中
  432.             push_frame(video_frame_queue_, frame);
  433.         }
  434.         // 释放内存
  435.         av_packet_unref(&pkt);
  436.     }
  437.     // 加载帧完成了,现在需要等待所有帧播放完毕
  438.     while(!video_frame_queue_.empty() || !audio_frame_queue_.empty())
  439.     {
  440.         printf("video_queue.size: %d ; audio_queue.size: %d --wait_over\n", video_frame_queue_.size(), audio_frame_queue_.size());
  441.         SDL_Delay(10);
  442.     }
  443.     // 标记线程结束了
  444.     thread_exit = 1;
  445.     system("pause");
  446.     return 0;
  447. }
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表