视频播放
不同的后缀表示不同的封装格式:把视频数据和音频数据打包成一个文件的规范
视频:连续时间内播放连续画面
帧率:fps(frames per second),1s多少张画面
动态补偿:MEMC
分辨率:横向像素数和纵向像素数的乘积
超高清 1080p 1920*1080。一个像素点是16位数据(BMP或PNG),那么一张1080p的图片的大小是4.14MB
一、视频播放器原理
1.解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据
2.解封装
将输入的封装格式的数据,分解成音频视频流压缩编码数据
\3. 解码
将视/音频压缩编码数据,解码为非压缩的视频/音频原始数据
ffm peg
\4. 视音频同步
根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来
1.1 流媒体协议
RTMP
1.2 音视频封装技术
FLV
1.3 视频压缩编码技术和音频压缩编码技术
H.264
人声识别:8k
位宽越大,声音越真实
观察者模式:发布者(推),订阅者(拉)
二、播放器解码ffmpeg以及简易同步
2.1 QT线程
QThread 定义子类 start() -> run()
通过信号和槽,实现QT内部通信
2.2. FFMPEG
对文件或网络资源进行解码,视频的播放采用YUY数据转换为RGB,绘图显示到控件
2.2.1 引入ffmpeg
1)添加头文件.h
extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include "libavdevice/avdevice.h" } ///由于我们建立的是C++的工程 ///编译的时候使用的C++的编译器编译 ///而 FFMPEG 是C的库 ///因此这里需要加上extern "C" ///否则会提示各种未定义
2)动态库引入.lib
INCLUDEPATH += $$PWD/ffmpeg-4.2.2/include LIBS += $$PWD/ffmpeg-4.2.2/lib/avcodec.lib\ $$PWD/ffmpeg-4.2.2/lib/avdevice.lib\ $$PWD/ffmpeg-4.2.2/lib/avfilter.lib\ $$PWD/ffmpeg-4.2.2/lib/avformat.lib\ $$PWD/ffmpeg-4.2.2/lib/avutil.lib\ $$PWD/ffmpeg-4.2.2/lib/postproc.lib\ $$PWD/ffmpeg-4.2.2/lib/swresample.lib\ $$PWD/ffmpeg-4.2.2/lib/swscale.lib ./ --- $$PWD
av:音视频
codec:编解码器
device:设备
filter:过滤器
format:主库
util:基础包
sw:转换
resample:重采样
scale:缩放
PWD:工程目录
3)添加动态库.dll
放到生成exe的目录下
int main(int argc, char *argv[]) { //这里简单的输出一个版本号 cout << "Hello FFmpeg!" << endl; av_register_all(); unsigned version = avcodec_version(); cout << "version is:" << version; return 0; }
2.2.2 FFmpeg解码
Ctrl+I 缩放补齐
void VideoPlayer::run() { qDebug()<<"VideoPlayer::"<<__func__; //1.初始化FFMPEG 调用了这个才能正常适用编码器和解码器 注册所用函数 av_register_all(); //2.需要分配一个AVFormatContext,FFMPEG 所有的操作都要通过这个AVFormatContext来进行 可以理解为视频文件指针 AVFormatContext *pFormatCtx = avformat_alloc_context(); //中文兼容 std::string path = m_fileName.toStdString(); qDebug()<<path.c_str(); const char* file_path = path.c_str(); //3. 打开视频文件 if( avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0 ) { qDebug()<<"can't open file"; return; } //3.1 获取视频文件信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { qDebug()<<"Could't find stream infomation."; return; } //4.读取视频流 int videoStream = -1; int i; for ( i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { videoStream = i; } } //如果videoStream 为-1 说明没有找到视频流 if (videoStream == -1) { qDebug()<< "Didn't find a video stream." ; return; } //如果videoStream 为-1 说明没有找到视频流 if (videoStream == -1) { qDebug()<< "Didn't find a video stream." ; return; } //5.查找解码器 auto pCodecCtx = pFormatCtx->streams[videoStream]->codec; auto pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { qDebug()<< "Codec not found." ; return; } //打开解码器 if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { qDebug()<< "Could not open codec." ; return; } //6.申请解码需要的结构体 AVFrame 视频缓存的结构体 AVFrame *pFrame, *pFrameRGB; pFrame = av_frame_alloc(); pFrameRGB = av_frame_alloc(); int y_size = pCodecCtx->width * pCodecCtx->height; AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个 packet av_new_packet(packet, y_size); //分配 packet 的数据 //7.这里我们将解码后的YUV数据转换成RGB32 YUV420p 格式视频数据-->RGB32--> 图片显示出来 struct SwsContext* img_convert_ctx; img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); auto numBytes =avpicture_get_size(AV_PIX_FMT_RGB32,pCodecCtx->width ,pCodecCtx->height); uint8_t* out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t)); avpicture_fill((AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_RGB32,pCodecCtx->width, pCodecCtx->height); //8.循环读取视频帧, 转换为RGB格式, 抛出信号去控件显示 int ret, got_picture; while(1){ //可以看出 av_read_frame读取的是一帧视频,并存入一个AVPacket的结构中 if (av_read_frame(pFormatCtx, packet) < 0){ break; //这里认为视频读取完了 } //生成图片 if (packet->stream_index == videoStream){ // 解码 packet存在pFrame里面 ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); if (ret < 0) { qDebug()<< "decode error"; return ; } //有解码器解码之后得到的图像数据都是YUV420的格式,而这里需要将其保存成图片文件 //因此需要将得到的YUV420数据转换成RGB格式 if (got_picture) { sws_scale(img_convert_ctx,(uint8_t const * const *) pFrame->data,pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,pFrameRGB->linesize); // 将 out_buffer 里面的数据存在 QImage里面 QImage tmpImg((uchar*)out_buffer,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32); //把图像复制一份 传递给界面显示 //显示到控件 多线程 无法控制控件 所以要发射信号 emit sig_getOneImage( tmpImg ); } } av_free_packet(packet); msleep(5); // 停一停 } }
2.2.2.1 函数:
编辑
编辑
编辑
编辑
2.2.2.2 流程:
2.3 简易同步
I帧:关键帧、主帧,包含一幅完整的图片信息,属于帧内编码图像。不含运动矢量,在解码时不需要参考其他帧图像
P帧:预测编码图像帧、差异帧,是帧间编码帧。利用之前的I帧或P帧进行预测编码。记录变化差异,推算运动矢量,减小内存
B帧:双向预测编码图像帧、双向差异帧,是帧间编码帧。利用之前和之后的I帧或P帧进行双向预测编码。更大的压缩率,但需要更多缓冲时间以及更高的CPU占用率。适合本地视频,不适合直播
编辑
PTS:显示时间戳
DTS:解码时间戳
简易同步思路:
#include"libavutil/time.h"
-
在解码之前看看是否需要等待
int64_t start_time = av_gettime(); int64_t pts = 0; //当前视频帧的pts //在解码之前看看是否需要等一下 int64_t realTime = av_gettime() - start_time; //主时钟时间 while(pts > realTime) { msleep(1); realTime = av_gettime() - start_time; //主时钟时间 }
-
获取视频时钟
//3)获取显示时间pts pts = pFrame->pts = pFrame->best_effort_timestamp; pts *= 1000000 * av_q2d(pFormatCtx->streams[videoStream]->time_base); //绝对时间
三、音频播放
3.1 基础知识
采样频率:每秒钟采样点的数量。人声至少4kHz,CD唱片44.1kHz,无损声音48kHz,超过48kHz人耳无法识别
采样精度(位宽):用多少位数据来表示一个音频数据点。位数越多,精度越高,采集的数据也就越真实。一般8~32位
声道数:声道越多,数据翻倍
比特率(码率):音频每秒的数据量,单位kbps(每秒多少千位)
例:48kHz的采样频率,16位位宽(2字节),双声道
1s 1个声道有48k采样点,每个采样点2字节,双声道,比特率=4822*8=1536kbps
3.2 SDL库
音频解码,需要ffmpeg。音频播放需要sdl
3.2.1 引入SDL
1)包含头文件.h
extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libavutil/pixfmt.h" #include "libswscale/swscale.h" #include "libavformat/avformat.h" #include "libavdevice/avdevice.h" #include <SDL.h> } #define SDL_AUDIO_BUFFER_SIZE 1024 #define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio //192000 的由来: 播放的音乐是48khz 32位的音频 这里主要计算采样率 就是1s采样多少字节 //32bit-> 4 字节 48khz 是1s 采样48000次 每次4字节 , 也就是1s采样 48000*4 = 192000字 节
2)添加引用
将ffmpeg和SDL2拷贝到工程目录下,在工程的pro文件中加入:
INCLUDEPATH += $$PWD/ffmpeg-4.2.2/include\ $$PWD/SDL2-2.0.10/include LIBS += $$PWD/ffmpeg-4.2.2/lib/avcodec.lib\ $$PWD/ffmpeg-4.2.2/lib/avdevice.lib\ $$PWD/ffmpeg-4.2.2/lib/avfilter.lib\ $$PWD/ffmpeg-4.2.2/lib/avformat.lib\ $$PWD/ffmpeg-4.2.2/lib/avutil.lib\ $$PWD/ffmpeg-4.2.2/lib/postproc.lib\ $$PWD/ffmpeg-4.2.2/lib/swresample.lib\ $$PWD/ffmpeg-4.2.2/lib/swscale.lib\ $$PWD/SDL2-2.0.10/lib/x86/SDL2.lib
3)添加动态库.dll
放到生成exe的目录下
运行时库dll 编译时库lib
4)#undef main
3.2.2 解码音频
使用生产者消费者控制
#include "PacketQueue.h" //同步队列 #define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000 //1 second of 48khz 32bit audio #define SDL_AUDIO_BUFFER_SIZE 1024 #define FILE_NAME "E:/QT/videoRes/2.mp3" #define ERR_STREAM stderr #define OUT_SAMPLE_RATE 44100 AVFrame wanted_frame; PacketQueue audio_queue; int quit = 0; //回调函数 void audio_callback(void *userdata, Uint8 *stream, int len); //解码函数 int audio_decode_frame(AVCodecContext *pcodec_ctx, uint8_t *audio_buf, int buf_size); //找 auto_stream int find_stream_index(AVFormatContext *pformat_ctx, int *video_stream, int *audio_stream); int main(int argc, char *argv[]) { //0.申请变量 //AV 文件视频流的”文件指针” AVFormatContext *pFormatCtx = NULL; int audioStream = -1;//解码器需要的流的索引 AVCodecContext *pCodecCtx = NULL;//解码器 AVCodec *pCodec = NULL; //解码器 AVPacket packet; // 解码前的数据 AVFrame *pframe = NULL; //解码之后的数据 char filename[256] = FILE_NAME; //SDL SDL_AudioSpec wanted_spec; //SDL 音频设置 SDL_AudioSpec spec; //SDL 音频设置 //1.ffmpeg 初始化 av_register_all(); //2.SDL初始化 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) { fprintf(ERR_STREAM, "Couldn't init SDL:%s\n", SDL_GetError()); exit(-1); } //3.打开文件 if (avformat_open_input(&pFormatCtx, filename, NULL, NULL) != 0) { fprintf(ERR_STREAM, "Couldn't open input file\n"); exit(-1); } //3.1 获取文件流信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { fprintf(ERR_STREAM, "Not Found Stream Info\n"); exit(-1); } //显示文件信息,十分好用的一个函数 av_dump_format(pFormatCtx, 0, filename, false); //4.读取音频流 if (find_stream_index(pFormatCtx, NULL, &audioStream) == -1) { fprintf(ERR_STREAM, "Couldn't find stream index\n"); exit(-1); } printf("audio_stream = %d\n", audioStream); //5.找到对应的解码器 pCodecCtx = pFormatCtx->streams[audioStream]->codec; pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (!pCodec) { fprintf(ERR_STREAM, "Couldn't find decoder\n"); exit(-1); } //6.设置音频信息, 用来打开音频设备。 wanted_spec.freq = pCodecCtx->sample_rate; //采样率 wanted_spec.format = AUDIO_S16SYS; //音频采样格式 表示16位 wanted_spec.channels = pCodecCtx->channels; //通道数 wanted_spec.silence = 0; //设置静音值 wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; //预期的采样点数 wanted_spec.callback = audio_callback;//回调函数 wanted_spec.userdata = pCodecCtx;//回调函数参数 //7.打开音频设备。 SDL_AudioDeviceID id = SDL_OpenAudioDevice( NULL ,0,&wanted_spec, &spec,0); if( id < 0 ) { //第二次打开 audio 会返回-1 fprintf(ERR_STREAM, "Couldn't open Audio: %s\n", SDL_GetError()); exit(-1); } //8.设置参数,供解码时候用, swr_alloc_set_opts的in部分参数 wanted_frame.format = AV_SAMPLE_FMT_S16; wanted_frame.sample_rate = spec.freq; //描述音频通道布局,用于指示某个音频流中各个声道的排列方式和数量 wanted_frame.channel_layout = av_get_default_channel_layout(spec.channels); wanted_frame.channels = spec.channels; //9.打开解码器, 初始化AVCondecContext,以及进行一些处理工作。 avcodec_open2(pCodecCtx, pCodec, NULL); //10.初始化队列 packet_queue_init(&audio_queue); //11. SDL播放声音 0播放 SDL_PauseAudioDevice(id,0); //12.循环读取音频帧(读一帧数据)放入音频同步队列 while(av_read_frame(pFormatCtx, &packet) >= 0) { if (packet.stream_index == audioStream) { packet_queue_put(&audio_queue, &packet); } else { av_free_packet(&packet); } } while( audio_queue.nb_packets != 0) { SDL_Delay(100); } //回收空间 avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); printf("play finished\n"); return 0; } int find_stream_index(AVFormatContext *pformat_ctx, int *video_stream, int *audio_stream){ assert(video_stream != NULL || audio_stream != NULL); int i = 0; int audio_index = -1; int video_index = -1; for (i = 0; i < pformat_ctx->nb_streams; i++) { if (pformat_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { video_index = i; } if (pformat_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) { audio_index = i; } } //注意以下两个判断有可能返回-1. if (video_stream == NULL) { *audio_stream = audio_index; return *audio_stream; } if (audio_stream == NULL) { *video_stream = video_index; return *video_stream; } *video_stream = video_index; *audio_stream = audio_index; return 0; }
-
设置音频信息, 用来打开音频设备:
wanted_spec:想要打开的 spec:实际打开的,可以不用这个,函数中直接用NULL,下面用到spec用wanted_spec代替。 这里会开一个线程,调用callback。 SDL_OpenAudioDevice->open_audio_device(开线程)->SDL_RunAudio->fill(指向callback函数)
-
打开音频设备: SDL_AudioDeviceID id = SDL_OpenAudioDevice( NULL ,0,&wanted_spec, &spec,0); 参数: 第一个为空表示默认设备 第二个参数0表示输出 1表示输入 参数: 第三个用于指定所需音频格式的 SDL_AudioSpec 结构体指针 参数: 第四个用于接收实际打开的音频设备信息的 SDL_AudioSpec 结构体指针。可以认为第三个参数是输入, 第四个参数是根据设备和输入得到的输出 参数: 第五个指示SDL可以在所需的规格中进行哪些更改,例如允许更改采样率或声道数等。如果不需要更改,则可以将此参数设置为0。
3.2.2.1 回调函数
//13.回调函数中将从队列中取数据, 解码后填充到播放缓冲区. void audio_callback(void *userdata, Uint8 *stream, int len) { AVCodecContext *pcodec_ctx = (AVCodecContext *) userdata; int len1, audio_data_size; static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2]; static unsigned int audio_buf_size = 0; static unsigned int audio_buf_index = 0; while (len > 0){ if (audio_buf_index >= audio_buf_size) { audio_data_size = audio_decode_frame(pcodec_ctx, audio_buf,sizeof(audio_buf)); /* audio_data_size < 0 标示没能解码出数据,我们默认播放静音 */ if (audio_data_size < 0) { /* silence */ audio_buf_size = 1024; /* 清零,静音 */ memset(audio_buf, 0, audio_buf_size); } else { audio_buf_size = audio_data_size; } audio_buf_index = 0; } /* 查看stream可用空间,决定一次copy多少数据,剩下的下次继续copy */ len1 = audio_buf_size - audio_buf_index; if (len1 > len) { len1 = len; } memset( stream , 0 , len1); //混音函数 sdl 2.0版本使用该函数 替换SDL_MixAudio SDL_MixAudioFormat(stream, (uint8_t *) audio_buf + audio_buf_index, AUDIO_S16SYS,len1,100); len -= len1; stream += len1; audio_buf_index += len1; } }
wanted_spec.callback = audio_callback;//回调函数 wanted_spec.userdata = pCodecCtx;//回调函数参数
回调函数 void audio_callback(void *userdata, Uint8 *stream, int len)
参数:userdata 是前面的AVCodecContext. 参数:此时是解码器 stream是要播放的缓冲区 参数:len 表示一次发送多少(采样点数)。 缓冲空间audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2]; 大小是最大音频空间的1.5倍, 目的不溢出 audio_buf_size:为样本缓冲区的大小,wanted_spec.samples.(一般每次解码这么多,文件不同,这个值不同) audio_buf_index: 标记发送到哪里了。( 三个变量设置为static就是为了保存上次数据,也可以用全局变量。)
len 是由SDL传入的SDL缓冲区的大小,如果这个缓冲未满,我们就一直往里填充数据 audio_buf_index 和 audio_buf_size 标示我们自己用来放置解码出来的数据的缓冲区,这些数据待copy到SDL缓冲区, 当audio_buf_index >= audio_buf_size的时候意味着我们的缓冲为空,没有数据可供copy,这时候需要调用audio_decode_frame来解码出更多的桢数据
回调函数的工作模式是:
-
解码数据放到audio_buf, 大小放audio_buf_size。
-
调用一次callback只能发送len个字节,而每次取回的解码数据可能比len大,一次发不完。
-
发不完的时候,会len为 0,不继续循环,退出函数,继续调用callback,进行下一次发送。
-
由于上次没发完,这次不取数据,发上次的剩余的,audio_buf_size标记发送到哪里了。
-
注意,callback每次一定要发且仅发len个数据,否则不会退出。如果没发够,缓冲区又没有了,就再取。发够了,就退出,留给下一个发,以此循环。
3.2.2.2 解码函数
//对于音频来说,一个packet里面,可能含有多帧(frame)数据。 int audio_decode_frame(AVCodecContext *pcodec_ctx, uint8_t *audio_buf, int buf_size){ static AVPacket pkt; static uint8_t *audio_pkt_data = NULL; static int audio_pkt_size = 0; int len1, data_size; int sampleSize = 0; AVCodecContext *aCodecCtx = pcodec_ctx; AVFrame *audioFrame = NULL; PacketQueue *audioq = &audio_queue; static struct SwrContext *swr_ctx = NULL; int convert_len; int n = 0; for(;;) { if( quit ) return -1; if(packet_queue_get(audioq, &pkt, 0) <= 0) {//一定注意 return -1; } audioFrame = av_frame_alloc(); audio_pkt_data = pkt.data; audio_pkt_size = pkt.size; while(audio_pkt_size > 0) { if( quit ) return -1; int got_picture; memset(audioFrame, 0, sizeof(AVFrame)); int ret =avcodec_decode_audio4( aCodecCtx, audioFrame, &got_picture, &pkt); if( ret < 0 ) { printf("Error in decoding audio frame.\n"); exit(0); } //一帧一个声道读取数据是nb_samples , channels为声道数 2表示16位2个字节 data_size = audioFrame->nb_samples * wanted_frame.channels * 2; if( got_picture ) { if (swr_ctx != NULL) { swr_free(&swr_ctx); swr_ctx = NULL; } swr_ctx = swr_alloc_set_opts(NULL, wanted_frame.channel_layout, (AVSampleFormat)wanted_frame.format,wanted_frame.sample_rate, audioFrame->channel_layout,(AVSampleFormat)audioFrame->format, audioFrame->sample_rate, 0, NULL); //初始化 if (swr_ctx == NULL || swr_init(swr_ctx) < 0) { printf("swr_init error\n"); break; } convert_len = swr_convert(swr_ctx, &audio_buf, AVCODEC_MAX_AUDIO_FRAME_SIZE, (const uint8_t **)audioFrame->data, audioFrame->nb_samples); } audio_pkt_size -= ret; if (audioFrame->nb_samples <= 0) { continue; } av_free_packet(&pkt); return data_size ; } av_free_packet(&pkt); } }
四、音视频混合
问题:视频流解码影响视频读取。改进:单独线程进行解码
把解码视频也放在视频队列中,一个线程负责解码后添加。把视频队列封装到类里
线程间通信:生产者消费者模型
同步:以音频时钟作为主时钟(解决快进等问题)
我们可以看出, 优化的程序实际由三个线程组成, QThread线程类的线程Run()函数, 相关参数的设置, 数据包的投递都是在这个线程来完成的, 而实际的音频和视频的解码工作, 都是在各自的线程中完成 的.
既然在各自的线程中完成, 那么就少不了一个问题, 就是如何做视频和音频的同步. 方法是, 在解码 的时候每次都获取一下音频时钟(音频是连续, 准确的, 以此作为主时钟), 方法是每次解码音频的时 候, 都计算音频时钟. 每一次解码视频的时候, 都获取一下当前视频对应显示时间 video_clock(pts), 如果发现视频显示时间大于音频的时间, 那么就延时(SDL_Delay(5) ) , 通过这种方式, 来让视音频 同步.
音频可以理解为均为I帧
音视频同步的特殊情况
如果没有音频作为同步,该怎么处理?
方案1: 测两帧图片的时间, 让其间隔时间超过正常的间隔时间
原方法是通过起始时间计算每帧时间,但是这样做遇到暂停等情况就没用了
double fps = av_q2d(is->video_st->r_frame_rate); double pts_diff = 1/ fps ; 可以计算出两帧之间的时差. 然后针对这个时间进行延时就可以了 is->cur_pts_time = av_gettime(); double fps = av_q2d(is->video_st->r_frame_rate); double pts_diff = 1/ fps ; while( is->last_pts_time != 0 && (is->cur_pts_time - is->last_pts_time < pts_diff*1000000) ) { SDL_Delay(1); is->cur_pts_time = av_gettime(); } 然后在发送完图片之后, 读取上一次的时间 if( is->audioStream == -1 ) is->last_pts_time = av_gettime();
因为涉及到线程切换(sleep()放弃当前时间片,进入就绪态,等待唤醒),会导致实际中每帧时间都会有延误,导致时间偏移越来越大
方案2: 使用SDL定时器
SDL_TimerID SDL_AddTimer(Uint32 interval/*表示定时器的触发间隔,单位为毫秒;*/, SDL_TimerCallback callback/*定时器触发时调用的回调函数*/, void *param/*表示传递给回调函数的参数*/); //返回一个 SDL_TimerID 类型的变量,表示创建的定时器的唯一标识符。如果创建定时器失败,则返回 0。 SDL_RemoveTimer(timer_id); //停止时, 可以通过id关闭定时器
五、播放控制
界面背景解决
可以重写控件环回事件
这里选择使用Widget设为黑色解决
-
将label作为Widget中的布局
-
将Widget底色设为黑色
为什么清晰度不如其他播放器?
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,\pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height); if (got_picture) { sws_scale(img_convert_ctx,(uint8_t const * const *) pFrame->data,pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,pFrameRGB->linesize); //把这个RGB数据 用QImage加载 QImage tmpImg((uchar*)out_buffer_rgb,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32); QImage image = tmpImg.copy(); //把图像复制一份 传递给界面显示 is->m_player->SendGetOneImage(image); //调用激发信号的函数 }
我们发现,在获取图片 进行转换时,会对图片的宽和高进行原比例的缩放
void PlayerDialog::slot_setImage(QImage img) { qDebug()<<__func__; //pixmap会有显卡渲染 image //缩放 QPixmap pixmap=QPixmap::fromImage(img.scaled(ui->lb_show->size(),Qt::KeepAspectRatio)); ui->lb_show->setPixmap(pixmap); }
然后这里又会按照控件大小进行一次缩放
解决:开始时将控件大小传入,在播放解码时直接按控件大小获取。当窗口的小变化时,使用resize()事件方法。这样少转换一次, //todo
1. 播放控制
本地数据库sqlite,存本地播放记录。
/// 播放控制的变量 bool readFinished; //读线程文件读取完毕 //可以看出 av_read_frame读取的是一帧视频,并存入一个AVPacket的结构中 if (av_read_frame(pFormatCtx, packet) < 0){ if( m_videoState.quit ) break; break; //这里认为视频读取完了 }
一般情况下,帧<0确实说明视频读完了,但是在网络传输中,可能会丢包,导致av_read_frame<0。设置一个延时,这段时间还是没收到包说明确实结束了,设置readFinished。 //todo
跳转相关的变量 int seek_req; //跳转标志 -- 读线程 int64_t seek_pos; //跳转的位置 -- 微秒 //主线程中点击进度条,设置seek_pos;然后在run线程中,若seek_req,就要读seek_pos int seek_flag_audio;//跳转标志 -- 用于音频线程中 int seek_flag_video;//跳转标志 -- 用于视频线程中 //视频跳转,要到队列中去找,清理掉不需要的 double seek_time; //跳转的时间(秒) 值和seek_pos是一样的 //用作同步
播放和暂停交替出现?
加一根弹簧,并且水平布局
stop
注意在视频解码线程函数中,需要把copy交给SendGetOneImage()
//把这个RGB数据 用QImage加载 QImage tmpImg((uchar *)out_buffer_rgb,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32); QImage image = tmpImg.copy(); //把图像复制一份 传递给界面显示 is->m_player->SendGetOneImage(image); //调用激发信号的函数]
QImage::QImage(uchar *data, int width, int height, QImage::Format format, QImageCleanupFunction cleanupFunction = nullptr, void *cleanupInfo = nullptr)
Constructs an image with the given width, height and format, that uses an existing memory buffer, data. The width and height must be specified in pixels, data must be 32-bit aligned, and each scanline of data in the image must also be 32-bit aligned.
函数中有一个指针指向堆区空间(out_buffer_rgb),这个空间在回收时会被回收掉,这时再用会出问题。所以要把他copy一下,让他有单独的空间,不会被QT马上回收(传参是浅拷贝)
2. 进度条 跳转功能
问题一:我们有2个队列,音频队列和视频队列,这2个队列里面的数据是可以播放几秒钟的,因此每次执 行跳转的时候需要同时将队列清空,所以先添加一个清空队列的函数:
问题二:按照上面的方式处理, 每次跳转的时候都会出现花屏的现象。 是因为解码器中保留了上一帧视频的信息,而现在视频发生了跳转,从而使得解码器中保留的信息 会对解码当前帧产生影响。 因此,清空队列的时候我们也要同时清空解码器的数据(包括音频解码器和视频解码器), 可以往队列中放入一个特殊的packet,当解码线程取到这个packet的时候,就执行清除解码器的 数据:
问题三:按照上面的方式处理, 每次跳转的时候都会出现花屏的现象。 是因为解码器中保留了上一帧视频的信息,而现在视频发生了跳转,从而使得解码器中保留的信息 会对解码当前帧产生影响。 因此,清空队列的时候我们也要同时清空解码器的数据(包括音频解码器和视频解码器), 可以往队列中放入一个特殊的packet,当解码线程取到这个packet的时候,就执行清除解码器的 数据:
这里选择不重写子类,使用事件过滤器
//事件过滤器 bool eventFilter(QObject* obj,QEvent* event); #include <QStyle> #include <QMouseEvent> bool PlayerDialog::eventFilter(QObject *obj, QEvent *event) { if (obj == ui->slider_progress) { if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event); int minn=ui->slider_progress->minimum(); int maxx=ui->slider_progress->maximum(); int value = QStyle::sliderValueFromPosition( minn, maxx, mouseEvent->pos().x(), width()); ui->slider_progress->setValue(value); m_player->seek((qint64)value*1000000); //value 秒 return true; } else { return false; } } else { // pass the event on to the parent class return QDialog::eventFilter(obj, event); } }
注册被观察
//安装事件过滤器,让该对象成为被观察对象 //this去执行函数 ui->slider_progress->installEventFilter(this);
控件焦点,应当集中在slider上