基于 音视频同步版本【基于外部时钟】 版本的优化,之前的每个代码版本都没有对获取到的包和帧进行内存释放,导致内存占用过多无法创建新的包崩溃了。这个版本:
- 创建普通变量packet对象,而不是指针packet了【这样方便调用av_packet_unref()。如果使用指针就需要在每次while的开头重新new一次,然后结尾在av_packet_free()一次,比较麻烦。】
- 在使用了包的地方加上了av_packet_unref()
- 在使用帧的地方加上了av_free_free()。
很奇怪!!!!
问题:在主线程的while读取包和帧的地方,第一句的printf有时候会导致程序崩溃。注释掉了就不会有问题,但是如果放开,那么有时候启动就会崩溃,有时候就不会崩溃。
- 多线程问题:
如果程序是多线程的,并且printf被多个线程同时调用,而没有适当的同步机制,则可能会导致数据竞争和崩溃。
答案:也许是这样?待确认
cpp
#include <iostream>
#include <windows.h>
#include<queue>
#include<chrono>
#include<ctime>
#ifdef __cplusplus ///
extern "C"
{
// 包含ffmpeg头文件
#include "libavutil/avutil.h"
#include"libavformat/avformat.h"
#include"libswscale/swscale.h"
#include"libswresample/swresample.h"
// 包含SDL头文件
#include"SDL.h"
}
#endif
using namespace std;
class AVSync{
public:
AVSync() {
}
void init()
{
start_time = getNowMilliseconds();
}
// 获取当前时间的pts应该是多少了
int getPts() {
return getNowMilliseconds() - start_time;
}
// 毫秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876】
Uint64 getNowMilliseconds() {
return getNowMicroseconds() / 1000;
}
// 微秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876543】
Uint64 getNowMicroseconds() {
using namespace std::chrono;
//
system_clock::time_point time_point_now = system_clock::now();
system_clock::duration duration = time_point_now.time_since_epoch();
return duration_cast<microseconds>(duration).count();
}
private:
// 音视频播放启动的时间--毫秒时间戳
Uint64 start_time = 0;
};
// 线程停止运行标识,0为正在运行,1为停止
int thread_exit = 0;
// 当前帧音频PCM数据
static Uint8 *audio_pcm_g;
// 当前帧音频PCM数据的字节总大小长度
static Uint32 audio_len_g;
// 音视频帧队列
queue<AVFrame*> audio_frame_queue_;
queue<AVFrame*> video_frame_queue_;
// 将main方法中的变量提取到全局以供两个线程函数中使用
AVFormatContext *input_fmt_ctx = NULL;
int video_idx = -1;
int audio_idx = -1;
AVCodecContext *audio_codec_ctx;
// 音视频同步工具类
AVSync sync_;
// 输出错误信息
void showError(int ret, const char *methodName = "method")
{
if(ret == 0) {
return ;
}
// 错误消息日志
char err2str[256];
// 将返回结果转化为字符串信息
av_strerror(ret, err2str, sizeof(err2str));
printf("%s failed, ret:%d, msg:%s\n", methodName, ret, err2str);
}
// 填充PCM数据到SDL中
void fill_audio_pcm(void *udata, Uint8 *stream, int len) {
// 清空上一帧的数据
SDL_memset(stream, 0, len);
// 如果外部线程【主线程读帧】还未读取到数据,那么无法填充PCM到SDL中进行播放
if(audio_len_g == 0)
{
return ;
}
// 本次回调结束最多只能取len字节的数据
// 如果外部读取的帧小于len字节,那么直接填充外部读取到的所有数据即可
// 如果外部读取的帧大于len字节,那么本次填充len字节的数据,等下次回调再填充 audio_len_g - len字节的数据
// 【如果audio_len_g - len 还是大于了len字节,那么继续取len填充即可】
len = len > audio_len_g ? audio_len_g : len;
//填充PCM数据到SDL中
SDL_MixAudio(stream, audio_pcm_g, len, SDL_MIX_MAXVOLUME/2);// SDL_MIX_MAXVOLUME/2 为音频大小,在0-128之间调整
// 更新pcm内存指针指向位置,已经【又】使用了len个字节空间,那么下次需要从当前位置+len的位置开始使用
audio_pcm_g += len;
// 更新剩余字节大小数量,已经读取了len个字节大小的数据,那么下次还剩 audio_len_g - len 个字节大小的数据可以使用
audio_len_g -= len;
}
// 视频播放线程
int play_video_thread(void *opaque) {
// SDL
// 初始化视频
if(SDL_Init(SDL_INIT_VIDEO)) {
return -1;
}
// 视频宽度
int video_width_ = input_fmt_ctx->streams[video_idx]->codecpar->width;
// 视频高度
int video_height_ = input_fmt_ctx->streams[video_idx]->codecpar->height;
// 创建窗口--显示器
// 在这里设置显示出来的窗口的总大小
SDL_Window *win_ = SDL_CreateWindow("苏花末测试窗口", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
video_width_, video_height_, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if(!win_) {
return -1;
}
// 渲染器,用于将纹理渲染到窗口上
SDL_Renderer *renderer_ = SDL_CreateRenderer(win_, -1, 0);
if(!renderer_) {
return -1;
}
// 纹理,用于设置渲染图片数据
SDL_Texture *texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, video_width_, video_height_);
if(!texture_) {
return -1;
}
// Rect--页面显示区域
SDL_Rect rect_;
// 刷新事件队列【防止有缓存】
SDL_PumpEvents();
SDL_Event event;
// 线程运行中
while(thread_exit == 0)
{
// 在没有事件的情况下才能刷新页面
if(SDL_PollEvent(&event) != 0) {
continue;
}
AVFrame *frame = video_frame_queue_.front();
// 帧的相对时间过去了多久【单位:1/刻度 秒】
double pts = frame->pts * av_q2d(input_fmt_ctx->streams[video_idx]->time_base);
printf("video.pts: %f ; now.pts: %f\n", pts, sync_.getPts() / 1000.0);
pts = pts * 1000;//转换毫秒
// 当前帧如果实际上应该播放的时间超过了当前时间,则代表当前帧应该在未来播放,现在不能播放
// 故在这里等待时间
if(pts > sync_.getPts())
{
SDL_Delay(pts - sync_.getPts());//等待时间直到时间到了pts,那么才播放
// 这里可以使用continue,或者也可以直接向下运行,加个continue方便盘逻辑
continue;
}
// 将当前帧移除待播放队列,因为这一帧马上就播放了
video_frame_queue_.pop();
// 如果当前帧已经延迟了500ms了,那么丢弃该帧,直接播放下一帧
if(pts < sync_.getPts() - 500)
{
continue;
}
// SDL: output
// 清空之前的页面
SDL_RenderClear(renderer_);
// 设置rect所占区域
rect_.x = 0;
rect_.y = 0;
// 在这里设置rect区域的大小,如果这里和窗口总大小不一样,那么其他地方是黑屏显示
// 故这里也体现了一个win可以设置多个rect,每个rect可以占据不同的位置
rect_.w = video_width_;
rect_.h = video_height_;
// 通过YUV格式渲染图片
SDL_UpdateYUVTexture(texture_, &rect_,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
// 页面内容设置
SDL_RenderCopy(renderer_, texture_, NULL, &rect_);
// 显示新的页面
SDL_RenderPresent(renderer_);
// 释放内存
av_frame_free(&frame);
}
return 0;
}
// 音频播放线程
int play_audio_thread(void *opaque) {
int ret = 0;
// 初始化音频
if(SDL_Init(SDL_INIT_AUDIO)) {
return -1;
}
// 音频播放上下文,音频播放只能通过这个结构体进行操作
// 创建 SwrContext 只能使用 swr_alloc() 函数
SwrContext *swrContext = swr_alloc();
if(!swrContext)
{
cout << "初始化swrContext对象失败" << endl;
return -1;
}
// 设置具体参数来创建 SwrContext对象
/* channel布局:如立体声、5.1声道、单声道等
* 采样格式:不同音频格式的采样格式不同,如AAC的采样格式是 AV_SAMPLE_FMT_FLTP,
* 而MP3的采样格式是 AV_SAMPLE_FMT_S16P
* 采样率:一秒钟采集多少次样本
* */
swrContext = swr_alloc_set_opts(NULL, //是否需要继承一个存在的SwrContext的内容
AV_CH_LAYOUT_STEREO, //输出的channel布局
AV_SAMPLE_FMT_S16, //输出的采样格式
44100, //输出的采样率
av_get_default_channel_layout(audio_codec_ctx->channels), //输入的channel布局
audio_codec_ctx->sample_fmt, //输入的采用格式
audio_codec_ctx->sample_rate, //输入的采用率
0,
NULL);
/* 为什么要重采样?
* 是因为输入的音频可能是mp4格式的,但是我们的电脑只能播放avi格式的音频,
* 所以需要转换数据,转换为确保我们的电脑一定能播放的格式。
* */
// 初始化重采样上下文
ret = swr_init(swrContext);
// 初始化重采样失败,那么音频无法播放
if(ret < 0)
{
cout << "初始化重采样上下文失败" << endl;
return -1;
}
// SDL_AudioSpc 是音频播放参数的结构体
// 期望能够实现的音频参数
SDL_AudioSpec wanted_spec;
wanted_spec.freq = 44100; //期望的采样率
wanted_spec.format = AUDIO_S16SYS; //期望的采样格式
wanted_spec.channels = 2; //期望的通道格式
wanted_spec.silence = 0; //期望中静音大小的值
wanted_spec.samples = 1024; //期望中一帧的数据大小,即样本数
wanted_spec.callback = fill_audio_pcm;//播放音频时会开启一个线程,反复调用这个回调函数,用来给音频填充PCM
wanted_spec.userdata = audio_codec_ctx; //回调函数中第一个参数的对象
// 按照指定参数打开真实的物理设备
ret = SDL_OpenAudio(&wanted_spec, NULL);
if(ret < 0)
{
cout << "打开音频设备失败" << endl;
return -1;
}
// 开始播放音频
SDL_PauseAudio(0);
// 分配输出音频数据
Uint8 *out_buffer = nullptr;
// 线程运行中
while(thread_exit == 0)
{
// 如果队列为空,则等待帧
if(audio_frame_queue_.empty())
{
SDL_Delay(1);
continue;
}
AVFrame *frame = audio_frame_queue_.front();
// 帧的相对时间过去了多久【单位:1/刻度 秒】
double pts = frame->pts * av_q2d(input_fmt_ctx->streams[audio_idx]->time_base);
printf("audio.pts: %f ; now.pts: %f\n", pts, sync_.getPts() / 1000.0);
pts = pts * 1000;//转换毫秒
// 当前帧如果实际上应该播放的时间超过了当前时间,则代表当前帧应该在未来播放,现在不能播放
// 故在这里等待时间
if(pts > sync_.getPts())
{
SDL_Delay(pts - sync_.getPts());//等待时间直到时间到了pts,那么才播放
// 这里可以使用continue,或者也可以直接向下运行,加个continue方便盘逻辑
continue;
}
// 将当前帧移除待播放队列,因为这一帧马上就播放了
audio_frame_queue_.pop();
// 如果当前帧已经延迟了500ms了,那么丢弃该帧,直接播放下一帧
if(pts < sync_.getPts() - 500)
{
continue;
}
// 获取输入的样本数
int in_samples = frame->nb_samples;
// 目标样本数【想要输出的样本数】
int dst_samples = av_rescale_rnd(in_samples, wanted_spec.freq, frame->sample_rate, AV_ROUND_UP);
// 计算需要输出的样本数内存空间大小
int out_buffer_size = av_samples_get_buffer_size(NULL, wanted_spec.channels,
dst_samples, AV_SAMPLE_FMT_S16, 0);
// 如果输出的音频数据未开辟过空间,那么开辟空间
if(!out_buffer)
{
// 输出数据的空间大小即为计算出来需要输出的样本数大小
out_buffer = (Uint8 *)av_malloc(out_buffer_size);
}
// 返回每个通道需要输出的样本数,错误时返回负值
int sample_count = swr_convert(swrContext, &out_buffer, dst_samples,
(const Uint8 **)frame->data, in_samples);// frame->data 即为采样到的数据
// 释放内存
av_frame_free(&frame);
// 获取不到样本数了,那么进行下一个包数据的读取
if(sample_count < 0)
{
break;
}
// 计算这一帧的字节数大小/长度
int out_size = sample_count * wanted_spec.channels *av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
// 如果回调函数中的字节数还未处理完,那么不能进行下一个音频帧的处理
while(audio_len_g > 0)
SDL_Delay(1);
// 回调函数中的字节已经处理完了,那么可以填充下一个音频帧需要的数据了
// 这一帧的字节长度
audio_len_g = out_size;
// 填充pcm数据
audio_pcm_g = (Uint8 *)out_buffer;
}
return 0;
}
// 将帧写入队列
void push_frame(queue<AVFrame*> &queue_, AVFrame *frame_) {
AVFrame *frame = av_frame_alloc();
av_frame_move_ref(frame, frame_);
queue_.push(frame);
}
#undef main
int main(int argc, char *argv[])
{
SetConsoleOutputCP(CP_UTF8);
if(argc < 2)
{
cout << "请输入视频地址" << endl;
return -1;
}
// 获取视频地址
char *url = argv[1];
// 方法调用结果
int ret = 0;
// FFmpeg
// AVFormatContext 是音视频开发使用到最多的结构体,无论什么函数基本上都会用到它
// AVFormatContext 只能通过 avformat_alloc_context() 创建空的对象
input_fmt_ctx = avformat_alloc_context();
// 加载视频内容到音视频格式上下文中
ret = avformat_open_input(&input_fmt_ctx, url, NULL, NULL);
// 输出日志
showError(ret);
// 查看流信息,可以不写,只是单纯拿返回值来做校验的
ret = avformat_find_stream_info(input_fmt_ctx, NULL);
// 输出日志
showError(ret);
// 输出视频信息,可以不写
av_dump_format(input_fmt_ctx, 0, url, 0);
// 查找指定流的idx,如果使用不到,可以不写; AVMEDIA_TYPE_VIDEO 代表视频流,AVMEDIA_TYPE_AUDIO代表音频流
video_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
audio_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
printf("video_idx: %d , audio_idx: %d\n", video_idx, audio_idx);
// AVCodecContext 是解码器上下文,需要对帧处理基本上都会用到它
// AVCodecContext 只能通过 avcodec_alloc_context3(NULL) 创建空的对象
// 视频解码器上下文
AVCodecContext *video_codec_ctx = avcodec_alloc_context3(NULL);
// 将音视频格式上下文中的参数加载到解码器上下文对象中
ret = avcodec_parameters_to_context(video_codec_ctx, input_fmt_ctx->streams[video_idx]->codecpar);
// 输出日志
showError(ret);
// 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容
AVCodec *video_codec = avcodec_find_decoder(video_codec_ctx->codec_id);
// 将物理解码器加载到解码器上下文中
ret = avcodec_open2(video_codec_ctx, video_codec, NULL);
// 音频解码器上下文
audio_codec_ctx = avcodec_alloc_context3(NULL);
// 将音视频格式上下文中的参数加载到解码器上下文对象中
ret = avcodec_parameters_to_context(audio_codec_ctx, input_fmt_ctx->streams[audio_idx]->codecpar);
// 输出日志
showError(ret);
// 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容
AVCodec *audio_codec = avcodec_find_decoder(audio_codec_ctx->codec_id);
// 将物理解码器加载到解码器上下文中
ret = avcodec_open2(audio_codec_ctx, audio_codec, NULL);
// 输出日志
showError(ret);
// 包,用来获取音视频格式上下文中的数据
// AVPacket 只能通过 av_packet_alloc() 创建对象
AVPacket pkt;
// 开启播放线程
SDL_CreateThread(play_video_thread, NULL, NULL);
SDL_CreateThread(play_audio_thread, NULL, NULL);
// 设置时钟
sync_.init();
// output and readFrame
while(1)
{
// printf("video_queue.size: %d ; audio_queue.size: %d\n", video_frame_queue_.size(), audio_frame_queue_.size());
// 防止读取内存过大
if(video_frame_queue_.size() >= 100 || audio_frame_queue_.size() >= 100)
{
SDL_Delay(1);
continue;
}
// FFmpeg: readFrame
// 获取该音视频格式上下文中的第一个包,并将从音视频格式上下文中移除
// 则代表了每次调用都会获取到新的包,之前的包不会再在该音视频格式上下文中找到了
ret = av_read_frame(input_fmt_ctx, &pkt);
// 如果包数据读取完毕,则代表视频播放结束了
if(ret < 0)
{
cout << "play video finish" << endl;
break;
}
// AVFrame 只能通过 av_frame_alloc() 创建对象
AVFrame *frame = av_frame_alloc();
// 音频帧
if(pkt.stream_index == audio_idx)
{
// 将包加载到解码器上下文中进行解码
ret = avcodec_send_packet(audio_codec_ctx, &pkt);
// 对应音频的包数据来说,一次包读取,可以获取到多个frame
while(1)
{
// 读取解码后的包中的帧
ret = avcodec_receive_frame(audio_codec_ctx, frame);
// 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧
// 如果所有的帧都读取完成了,那么开始读取下一个包
if(ret == AVERROR(EAGAIN))
{
break;
}
// 将帧添加到队列中
push_frame(audio_frame_queue_, frame);
}
}
// 视频帧
else if(pkt.stream_index == video_idx)
{
// 将包加载到解码器上下文中进行解码
ret = avcodec_send_packet(video_codec_ctx, &pkt);
// 读取解码后的包中的帧
ret = avcodec_receive_frame(video_codec_ctx, frame);
// 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧
if(AVERROR(EAGAIN) == ret)
{
continue;
}
// 将帧添加到队列中
push_frame(video_frame_queue_, frame);
}
// 释放内存
av_packet_unref(&pkt);
}
// 加载帧完成了,现在需要等待所有帧播放完毕
while(!video_frame_queue_.empty() || !audio_frame_queue_.empty())
{
printf("video_queue.size: %d ; audio_queue.size: %d --wait_over\n", video_frame_queue_.size(), audio_frame_queue_.size());
SDL_Delay(10);
}
// 标记线程结束了
thread_exit = 1;
system("pause");
return 0;
}