ffmpeg编码实现音视频

视频播放

不同的后缀表示不同的封装格式:把视频数据和音频数据打包成一个文件的规范

视频:连续时间内播放连续画面

帧率:fps(frames per second),1s多少张画面

动态补偿:MEMC

分辨率:横向像素数和纵向像素数的乘积

超高清 1080p 1920*1080。一个像素点是16位数据(BMP或PNG),那么一张1080p的图片的大小是4.14MB

一、视频播放器原理

1.解协议

将流媒体协议的数据,解析为标准的相应的封装格式数据

2.解封装

将输入的封装格式的数据,分解成音频视频流压缩编码数据

\3. 解码

将视/音频压缩编码数据,解码为非压缩的视频/音频原始数据

ffm peg

\4. 视音频同步

根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来

1.1 流媒体协议

img

RTMP

1.2 音视频封装技术

img

FLV

1.3 视频压缩编码技术和音频压缩编码技术

img

H.264

img

img

人声识别:8k

位宽越大,声音越真实

img

img

img

观察者模式:发布者(推),订阅者(拉)

二、播放器解码ffmpeg以及简易同步

2.1 QT线程

QThread 定义子类 start() -> run()

通过信号和槽,实现QT内部通信

2.2. FFMPEG

对文件或网络资源进行解码,视频的播放采用YUY数据转换为RGB,绘图显示到控件

img

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 函数:

imgimg编辑

img编辑

img编辑

img编辑

2.2.2.2 流程:

img

2.3 简易同步

I帧:关键帧、主帧,包含一幅完整的图片信息,属于帧内编码图像。不含运动矢量,在解码时不需要参考其他帧图像

P帧:预测编码图像帧、差异帧,是帧间编码帧。利用之前的I帧或P帧进行预测编码。记录变化差异,推算运动矢量,减小内存

B帧:双向预测编码图像帧、双向差异帧,是帧间编码帧。利用之前和之后的I帧或P帧进行双向预测编码。更大的压缩率,但需要更多缓冲时间以及更高的CPU占用率。适合本地视频,不适合直播

img编辑

PTS:显示时间戳

DTS:解码时间戳

简易同步思路:

img

#include"libavutil/time.h"

  1. 在解码之前看看是否需要等待

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; //主时钟时间
    }

  1. 获取视频时钟

        //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;
}
  1. 设置音频信息, 用来打开音频设备:

wanted_spec:想要打开的 spec:实际打开的,可以不用这个,函数中直接用NULL,下面用到spec用wanted_spec代替。 这里会开一个线程,调用callback。 SDL_OpenAudioDevice->open_audio_device(开线程)->SDL_RunAudio->fill(指向callback函数)

  1. 打开音频设备: 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来解码出更多的桢数据

回调函数的工作模式是:

  1. 解码数据放到audio_buf, 大小放audio_buf_size。

img

  1. 调用一次callback只能发送len个字节,而每次取回的解码数据可能比len大,一次发不完。

  2. 发不完的时候,会len为 0,不继续循环,退出函数,继续调用callback,进行下一次发送。

  3. 由于上次没发完,这次不取数据,发上次的剩余的,audio_buf_size标记发送到哪里了。

img

  1. 注意,callback每次一定要发且仅发len个数据,否则不会退出。如果没发够,缓冲区又没有了,就再取。发够了,就退出,留给下一个发,以此循环。

img

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);
    }
}

img

四、音视频混合

image-20240806085637464

问题:视频流解码影响视频读取。改进:单独线程进行解码

把解码视频也放在视频队列中,一个线程负责解码后添加。把视频队列封装到类里

image-20240806100207479

线程间通信:生产者消费者模型

同步:以音频时钟作为主时钟(解决快进等问题)

我们可以看出, 优化的程序实际由三个线程组成, QThread线程类的线程Run()函数, 相关参数的设置, 数据包的投递都是在这个线程来完成的, 而实际的音频和视频的解码工作, 都是在各自的线程中完成 的.

既然在各自的线程中完成, 那么就少不了一个问题, 就是如何做视频和音频的同步. 方法是, 在解码 的时候每次都获取一下音频时钟(音频是连续, 准确的, 以此作为主时钟), 方法是每次解码音频的时 候, 都计算音频时钟. 每一次解码视频的时候, 都获取一下当前视频对应显示时间 video_clock(pts), 如果发现视频显示时间大于音频的时间, 那么就延时(SDL_Delay(5) ) , 通过这种方式, 来让视音频 同步.

image-20240806100657062

音频可以理解为均为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设为黑色解决

  1. 将label作为Widget中的布局

  2. 将Widget底色设为黑色

image-20240807103644299

image-20240807103721883

为什么清晰度不如其他播放器

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是一样的 
//用作同步

播放和暂停交替出现?

加一根弹簧,并且水平布局

image-20240808113845538

image-20240808113827365

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上

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值