(三) FFmpeg结合Qt实现视频播放器(一)

目录

前言

(二) FFmpeg解码视频学习已经完成了Qt实现视频播放器的一部分,但是没有音频,而且在播放大的视频的时候,甚至会到导致我的Ubuntu卡死。继续跟着大神们学习!

SDL的使用

SDL概述

主流的音频开源库:OpenAL、PortAudio、SDL、SDL_audioin。其中SDL资料多,学习方便;跨平台;库体积相对比较小。但是SDL**不能采集音频**,FFmpeg支持音频采集,必要的时候可以直接使用FFmpeg采集。SDL本身是一个多媒体库,其最强大的地方不是在音频上,而是在图形显示上。这里只是使用SDL来播放音频。

SDL下载地址

SDL下载地址

SDL的linux下编译

配置选项:

./configure CC=arm-linux-gnueabihf-gcc --host=arm-linux --prefix=/grapeRain/grape_SDL2 --disable-static --enable-shared --disable-pulseaudio --disable-esd
make -j4
make install

注:--disable-pulseaudio --disable-esd是因为编译之后出现错误,所以我disable了这两个选项,之后编译就通过了,所以我也不知道这两个编译选项的含义。

SDL的使用

参考从零开始学习音视频编程技术(七) FFMPEG Qt视频播放器之SDL的使用中提供的项目源码。
注意:在FFmpeg3.4.1这个版本中,avcodec_alloc_frame()函数已经废弃了,使用av_frame_alloc()替换。同时,avcodec_get_frame_defaults()也已经废弃,具体可以查看FFmpeg的文档。文档路径为:/xxx/ffmpeg-3.4.1/doc/下的APIchanges,里面记录了FFmpegAPI改动。
参考FFmpeg API 变更记录


SDL播放音频是通过回调函数的方式播放,且这个回调函数是在新的线程中运行,此回调函数固定时间激发一次,这个时间和要播放的音频频率有关系。(和Qt的定时器有点像)
因此,用FFmpeg读到一帧音频后,不是着急解码,而是将数据存入一个队列,等SDL回调函数激发的时候,从这个队列中取出数据,然后解码播放。

SDL的播放音频

播放音频流程

  1. 初始化
    1) 初始化SDL
    2) 根据参数(SDL_AudioSpec)打开音频设备
  2. 循环播放数据
    1) 播放音频数据
    2) 延时等待播放完成

SDL结构体

  • SDL_AudioSpec
typedef struct SDL_AudioSpec  
{  
    int freq;                   音频数据采样率 常用48000 44100
    SDL_AudioFormat format;     音频数据的格式。
    Uint8 channels;             声道数。例如单声道取值为1,立体声取值为2
    Uint8 silence;              设置静音的值
    Uint16 samples;             音频缓冲区中的采样个数,要求必须是2的n次方
    Uint16 padding;             考虑到兼容性的一个参数
    Uint32 size;                音频缓冲区的大小,以字节为单位
    SDL_AudioCallback callback; 填充音频缓冲区的回调函数
    void *userdata;             用户自定义的数据
} SDL_AudioSpec; 

注:SDL_AudioFormat format;—-常用的音频数据格式:

AUDIO_U16SYS:unsigned 16-bit samples
AUDIO_S16SYS:Signed 16-bit samples
AUDIO_S32SYS:32-bit integer samples
AUDIO_F32SYS:32-bit floating point samples

SDL的API

  • SDL_Init()
// 功能:初始化SDL
int SDLCALL SDL_Init(Uint32 flags);
// 参数:flags
SDL_INIT_TIMER:定时器
SDL_INIT_AUDIO:音频
SDL_INIT_VIDEO:视频
SDL_INIT_JOYSTICK:摇杆
SDL_INIT_GAMECONTROLLER:游戏控制器
SDL_INIT_NOPARACHUTE:不捕获关键信号
SDL_INIT_EVERYTHING:包含上述所有选项
  • SDL_OpenAudio()
    SDL_OpenAudio打开SDL播放设备
  • SDL_PauseAudio()
// 功能:当pause_on设置为0即开始播放音频数据。设置为1的时候,将会播放静音的值
void SDLCALL SDL_PauseAudio(int pause_on)
  • 回调函数
    当音频设备需要更多数据时,会调用这个回调函数。回调函数格式如:
void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,  
                                            int len);  
userdata: SDL_AudioSpec结构中的用户自定义数据,一般情况可以不用。
stream: 该指针指向需要填充的音频缓冲区

SDL互斥量和条件变量

在进行音视频播放的过程中,会把解复用和解码放在不同的线程中,存放包的队列是公共资源,需要互斥访问。具体就是解复用向队列中添加包,解码从队列中取包,也就需要同步。所以在队列的入队和出对操作中,采用了互斥量和条件变量。

互斥量

  • 创建互斥量
SDL_mutex *mutex;
mutex = SDL_CreateMutex();

创建的互斥量默认是未上锁的。
- 上锁和解锁

SDL_LockMutex(mutex);
SDL_UnlockMutex(mutex);
  • 销毁锁
SDL_DestroyMutex(mutex);

条件变量

  • 创建条件变量
SDL_cond* SDL_CreateCond(void) 
  • 等待条件变量
int SDL_CondWait(SDL_cond*  cond,  
                 SDL_mutex* mutex) 

信号激活后,返回0,否则返回错误代码。
SDL_CondWait必须在互斥量锁住之后才能调用。该函数会解锁锁住的互斥量,并等待拥有该锁的线程激活信号。激活后,重新上锁。
- 激活信号

int SDL_CondSignal(SDL_cond* cond)  

SDL_CondSignal会激活等待的一个线程(根据优先级),而不是所有等待的线程。


Linux 线程同步—条件变量

代码

typedef struct PacketQueue
{
    AVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    SDL_mutex *mutex;
    SDL_cond *cond;
}PacketQueue_t;
void packet_queue_init(PacketQueue *q)
{
    memset(q, 0, sizeof(PacketQueue));
    q->mutex = SDL_CreateMutex();
    q->cond  = SDL_CreateCond();
    q->size  = 0;
    q->nb_packets = 0;
    q->first_pkt  = NULL;
    q->last_pkt   = NULL;
}

int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    AVPacketList *pkt1;
    if (av_dup_packet(pkt) < 0) {
        return -1;
    }

    pkt1 = (AVPacketList*)av_malloc(sizeof(AVPacketList));
    if (!pkt1)
    {
        return -1;
    }
    pkt1->pkt = *pkt;
    pkt1->next = NULL;

    SDL_LockMutex(q->mutex);
    if(!q->last_pkt)
    {
        q->first_pkt = pkt1;
    }
    else
    {
        q->last_pkt->next = pkt1;
    }

    q->last_pkt = pkt1;             // pointer to the add one packet
    q->nb_packets++;
    q->size += pkt1->pkt.size;

    SDL_CondSignal(q->cond);
    SDL_UnlockMutex(q->mutex);

    return 0;
}

int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
    AVPacketList *pkt1;
    int ret;

    SDL_LockMutex(q->mutex);

    for(;;)
    {
        pkt1 = q->first_pkt;
        if(pkt1)
        {
            q->first_pkt = pkt1->next;
            if(!q->first_pkt)
            {
                q->last_pkt = NULL;
            }
            q->nb_packets--;
            q->size -= pkt1->pkt.size;
            av_free(pkt);
            ret = 1;
            break;
        }
        else if(!block)
        {
            ret = 0;
            break;
        }
        else
        {
            SDL_CondWait(q->cond, q->mutex);
        }
    }

    SDL_UnlockMutex(q->mutex);

    return ret;
}

理解来说,假设packet_queue_put是一个生产者,一直往队列里填数据;而packet_queue_get是消费者,一直从队列中取数据。当packet_queue_get从队列中取数据时,发现队列中已经没有数据了,就会调用SDL_CondWait进入等待状态(阻塞),同时释放互斥锁。这时候,packet_queue_put往队列中填入数据后,调用SDL_CondSignal,唤醒packet_queue_get读取数据,并重新上锁。在读取数据完毕之后,释放锁。
博主记:以上是博主的个人理解,可能有不对的地方,见谅!!

FFmpeg函数解析

结构体

  • AVPacket
    每一个Packet是一个完整的帧,用来暂存解复用之后、解码之前的媒体数据
typedef struct AVPacket  
{  
    int64_t pts;    // 显示时间戳
    int64_t dts;    // 解码时间戳
    int64_t pos;  
    uint8_t *data;  // 数据首地址  
    int size;  
    int stream_index; // 所属媒体流的索引  
    int flags;        // flags为标志域,1表示该数据是一个关键帧  
    void(*destruct)(struct AVPacket*);  // 释放数据缓冲区的函数指针  
} AVPacket;  

AVPacket本身只是一个容器,其中data成员指向实际的数据缓冲区。这个缓冲区通常由av_new_packet创建,也可能由FFMPEG的API创建。当某个AVPacket结构的数据缓冲区不再被使用时,需要通过调用av_free_packet释放。

FFmpeg内部使用AVPacket建立缓冲区装载数据,同时提供destruct函数,如果FFmpeg打算自己维护缓冲区,则将destruct设为av_destruct_packet_nofree,用户调用av_free_packet清理缓冲区时并不能将其释放(共享缓冲区);如果FFmpeg打算将该缓冲区彻底交给调用者,则将destruct设为av_destruct_packet,表示它能够被释放。安全起见,如果用户希望自由使用一个FFmpeg内部创建的AVPacket,最好调用av_dup_packet进行缓冲区的克隆,将其转化为缓冲区能够被释放的AVPacket,以免对缓冲区的不当占用造成异常错误。av_dup_packet会为destruct指针为av_destruct_packet_nofreeAVPacket新建一个缓冲区,然后将原缓冲区的数据拷贝至新缓冲区,设置data的值为新缓冲区的地址,同时设置destruct指针为av_destruct_packet
- AVPacketList

typedef struct AVPacketList {  
    AVPacket pkt;       // AVPacket 
    struct AVPacketList *next; // next是一个AVPacketList指针  
} AVPacketList;  
  • PacketQueue
typedef struct PacketQueue {  
  AVPacketList *first_pkt, *last_pkt; // first_pkt指向AVPacketList的头 last_pkt指向AVPacketList的尾  
  int nb_packets;  // Packet队列的中AV_Packet的个数  
  int size;  
  SDL_mutex *mutex;  
  SDL_cond *cond;  
} PacketQueue;

函数

  • av_dup_packet(AVPacket* pkt)
int av_dup_packet(AVPacket *pkt)
{
    if (((pkt->destruct == av_destruct_packet_nofree) || (pkt->destruct == NULL)) && pkt->data) {
    uint8_t *data;

    if((unsigned)pkt->size > (unsigned)pkt->size + FF_INPUT_BUFFER_PADDING_SIZE)
    return AVERROR(ENOMEM);
    data = av_malloc(pkt->size + FF_INPUT_BUFFER_PADDING_SIZE);
    if (!data) {
        return AVERROR(ENOMEM);
    }
    memcpy(data, pkt->data, pkt->size);
    memset(data + pkt->size, 0, FF_INPUT_BUFFER_PADDING_SIZE);
    pkt->data = data;
    pkt->destruct = av_destruct_packet;
    }
    return 0;
}

av_destruct_packet_nofree表示AVPacket由FFmpeg维护,换句话来说,就是由FFmpeg来管理释放。既然由FFmpeg来管理,作用可以用于共享,推测就是数据可以拷贝不可以被随意修改,如果需要数据被修改,需要使用av_dup_packet拷贝一份数据。

总结

参考链接

最简单的视音频播放示例9:SDL2播放PCM
FFmpeg API 变更记录
从零开始学习音视频编程技术(十) FFMPEG Qt视频播放器之播放控制
ffmpeg之AVPacket笔记
FFMPEG AVPacket
SDL中的互斥量和条件变量
Linux 线程同步—条件变量

展开阅读全文

没有更多推荐了,返回首页