(二) FFmpeg解码视频学习已经完成了Qt实现视频播放器的一部分,但是没有音频,而且在播放大的视频的时候,甚至会到导致我的Ubuntu卡死。继续跟着大神们学习!
主流的音频开源库:OpenAL、PortAudio、SDL、SDL_audioin。其中SDL资料多,学习方便;跨平台;库体积相对比较小。但是SDL**不能采集音频**,FFmpeg支持音频采集,必要的时候可以直接使用FFmpeg采集。SDL本身是一个多媒体库,其最强大的地方不是在音频上,而是在图形显示上。这里只是使用SDL来播放音频。
SDL下载地址
配置选项:
./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了这两个选项,之后编译就通过了,所以我也不知道这两个编译选项的含义。
参考从零开始学习音视频编程技术(七) FFMPEG Qt视频播放器之SDL的使用中提供的项目源码。
注意:在FFmpeg3.4.1这个版本中,avcodec_alloc_frame()
函数已经废弃了,使用av_frame_alloc()
替换。同时,avcodec_get_frame_defaults()
也已经废弃,具体可以查看FFmpeg的文档。文档路径为:/xxx/ffmpeg-3.4.1/doc/下的APIchanges
,里面记录了FFmpeg
的API
改动。
参考FFmpeg API 变更记录
SDL
播放音频是通过回调函数的方式播放,且这个回调函数是在新的线程中运行,此回调函数固定时间激发一次,这个时间和要播放的音频频率有关系。(和Qt的定时器有点像)
因此,用FFmpeg读到一帧音频后,不是着急解码,而是将数据存入一个队列,等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_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_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
读取数据,并重新上锁。在读取数据完毕之后,释放锁。
博主记:以上是博主的个人理解,可能有不对的地方,见谅!!
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_nofree
的AVPacket
新建一个缓冲区,然后将原缓冲区的数据拷贝至新缓冲区,设置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 线程同步—条件变量