ffmpeg播放器实现详解 - 音频播放

2023-05-16

1、生产者-消费者线程模型


本文主要讨论posix标准下的生产者-消费者线程模型,posix标准多用于类linux相关环境

POSIX: The Portable Operating System Interface (POSIX) is a family of standards specified by the IEEE Computer Society for maintaining compatibility between operating systems. POSIX defines the application programming interface (API), along with command line shells and utility interfaces, for software compatibility with variants of Unix and other operating systems

1.1 posix线程模型

生产者-消费者(producer-consumer)问题是一个经典的线程同步问题,它可以描述为两个或者多个线程共同维护同一个临界区资源(critical resource),其中,生成者线程负责从网络接口或本地视频文件中抽取数据,并向临界区注入数据,这里的数据可以是解码后的音视频帧,或者音视频编码数据包,消费者线程负责从临界区抽取数据,并对数据进行处理,例如对解码后的视频帧进行渲染,对音频帧进行播放,或者是从队列中提取音视频编码数据包并解码。下图为生产者-消费者线程同步模型示意图。

图中上面的process_msg为消费者线程,下面的enqueue_msg为生产者线程,从左到右代表了线程执行的时间线。

我们先来看消费者线程,消费者线程率先获取互斥锁对象(图中的红点表示互斥锁对象),获得了对临界区资源的独占处理权及cpu资源的优先使用权,然后开始执行自己的线程函数。

在消费者的线程函数中,首先检查临界区资源是否满足执行条件,如队列是否已经存在待解码的视频编码包,满足执行条件,则从队列中取出数据执行自己的逻辑。

如果临界区资源不满足执行条件,如队列为空,此时,消费者线程通过在pthread_cond_wait中临时释放互斥锁,并将自己投入休眠状态,等待被生产者线程向临界区注入数据,将自己唤醒并重新获得互斥锁,这时消费者线程会阻塞在pthread_cond_wait调用中

消费者线程通过在pthread_cond_wait中临时释放互斥锁后,将自己投入休眠状态,此时生成者线程将获得互斥锁,并获得了对临界区资源的独占处理权及cpu资源的优先使用权,然后开始执行自己的线程函数。生成者线程向临界区资源注入数据,如向队列中注入待解码的数据包,然后,通过pthread_cond_signal唤醒消费者线程(图中虚线所示),随即通过unlock_mutex释放互斥锁

在生成者线程释放互斥锁后,消费者线程已被唤醒,并重新获取互斥锁,再次检查临界区资源,如果满足条件,则执行自己的线程函数,然后释放互斥锁,等待下一次执行。这里需要说明一下,在实际情况下,并不总是消费者线程优先获得互斥锁,这是由cpu调度决定的。下面给出一些示例代码来描述这个过程。

//1、消息队列处理函数在处理消息前,先对互斥量进行锁定,以保护消息队列中的临界区资源

//2、若消息队列为空,则调用pthread_cond_wait对互斥量暂时解锁,等待其他线程向消息队列中插入消息数据

//3、待其他线程向消息队列中插入消息数据后,通过pthread_cond_signal向等待线程发出qready信号

//4、消息队列处理线程收到qready信号被唤醒,重新获得对消息队列临界区资源的独占

#include <pthread.h>

struct msg{//消息队列结构体

struct msg *m_next;//消息队列后继节点

//more stuff here

}

struct msg *workq;//消息队列指针

pthread_cond_t qready=PTHREAD_COND_INITIALIZER;//消息队列就绪条件变量

pthread_mutex_t qlock=PTHREAS_MUTEX_INITIALIZER;//消息队列互斥量,保护消息队列数据

//消息队列处理函数

void process_msg(void){

struct msg *mp;//消息结构指针

for(;;){

pthread_mutex_lock(&qlock);//消息队列互斥量加锁,保护消息队列数据

while(workq==NULL){//检查消息队列是否为空,若为空

pthread_cond_wait(&qready,&qlock);//等待消息队列就绪信号qready,并对互斥量暂时解锁,该函数返回时,互斥量再次被锁住

}

mp=workq;//线程醒来,从消息队列中取数据准备处理

workq=mp->m_next;//更新消息队列,指针后移清除取出的消息

pthread_mutex_unlock(&qlock);//释放锁

//now process the message mp

}

}

//将消息插入消息队列

void enqueue_msg(struct msg *mp){

pthread_mutex_lock(&qlock);//消息队列互斥量加锁,保护消息队列数据

mp->m_next=workq;//将原队列头作为插入消息的后继节点

workq=mp;//将新消息插入队列

pthread_cond_signal(&qready);//给等待线程发出qready消息,通知消息队列已就绪

pthread_mutex_unlock(&qlock);//释放锁

}

1.2 SDL线程模型

本文例程中出现的sdl线程模型,它的使用方法与posix线程模型完全相同,可以看作sdl库对pthread线程组件的封装,其中,SDL_cond可以看作是sdl对pthread_cond_t的封装,其他sdl线程组件对应同名的pthread线程组件

  • pthread_mutex_t - SDL_mutex

  • pthread_cond_t - SDL_cond

  • pthread_mutex_lock - SDL_LockMutex

  • pthread_mutex_unlock - SDL_UnlockMutex

  • pthread_cond_wait - SDL_CondWait

  • pthread_cond_signal - SDL_CondSignal

2、音频播放


在上篇文章中我们讨论了如何对视频帧进行渲染显示,虽然画面已经有了,但还缺少声音,本文在上篇文章的基础上,继续完善我们的播放器开发,讨论如何播放声音。

2.1 音频播放前的准备

在音频帧播放前,首先要有一个存储音频编码数据包的缓存队列PacketQueue,用于保存从网络接口或本地视频文件中抽取的编码数据,packet_queue_put负责向缓存队列中填充编码数据包,packet_queue_get负责从队列中提取数据包,packet_queue_put与packet_queue_get之间构成了生产者与消费者关系,生产者首先检查缓存队列是否有足够的空间,若队列存在剩余空间,则向队列注入数据,然后发送信号唤醒消费者线程,若队列满则生产者线程进入休眠

消费者检查缓存队列状态,若队列为空则进入休眠模式,若队列满则从队列中抽取音视频编码数据包交给解码器处理,当队列为空时向生产者发送信号请求数据,同时自己进入休眠状态

例程中生产者-消费者工作原理与1.1节内容完全相同

2.2 音频输出回调函数

sdl库通过SDL_OpenAudio打开音频设备,并创建音频处理后台线程,sdl后台线程通过audio_callback回调函数将解码后的pcm数据送入声卡播放。sdl通常一次会准备一组缓存pcm数据,通过该回调送入声卡,声卡根据音频pts依次播放pcm数据,待送入缓存的pcm数据完成播放后,再载入一组新的pcm缓存数据(每次音频输出缓存为空时,sdl就调用此函数填充音频输出缓存,并送入声卡播放)

2.3 音频播放参数设置

通过创建SDL_AudioSpec结构体,设置音频播放参数

// Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format

// 创建SDL_AudioSpec结构体,设置音频播放参数

wanted_spec.freq = aCodecCtx->sample_rate;//采样频率 DSP frequency -- samples per second

wanted_spec.format = AUDIO_S16SYS;//采样格式 Audio data format

wanted_spec.channels = aCodecCtx->channels;//声道数 Number of channels: 1 mono, 2 stereo

wanted_spec.silence = 0;//无输出时是否静音

wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;//默认每次读音频缓存的大小,推荐值为 512~8192,ffplay使用的是1024 specifies a unit of audio data refers to the size of the audio buffer in sample frames

wanted_spec.callback = audio_callback;//设置取音频数据的回调接口函数 the function to call when the audio device needs more data

wanted_spec.userdata = aCodecCtx;//传递用户数据

2.4 流程图

例程的整体工作流程如下图所示。其中,avcodec_decode_audio4函数用于音频数据的解码,解码操作结束后,会再次进入到audio_callback回调函数的逻辑中,audio_callback回调函数工作在sdl后台线程中,流程图中从avcodec_decode_audio4到audio_callback的连线表示该过程会反复执行。packet_queue_put与packet_queue_get之前共同维护PacketQueue缓存队列,二者SDL_cond方式进行同步

3、源码编译验证


源码的编译方法和之前的例程完全相同,源码可采用如下Makefile脚本进行编译

tutorial03: tutorial03.c

gcc -o tutorial03 -g3 tutorial03.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} \

-L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \

`sdl-config --cflags --libs`

clean:

rm -rf tutorial03

执行make命令开始编译,编译完成后,可在源码目录生成名为[tutorial03]的可执行文件。与ffplay的使用方法类似,执行[tutorial03 url]命令,除了有画面显示外可以听到有声音的输出,但此时的声音播放功能还无法正常工作,别着急,后面的内容会在此基础上继续完善,直到最终实现一个能够正常播放视频的播放器为止

./tutorial03 rtmp://58.200.131.2:1935/livetv/hunantv

输入Ctrl+C结束程序运行

4、源码清单


源码在上篇的内容基础上,主要增加音频缓存队列处理,音频解码,音频播放等几个部分,源码几乎每行都有注释,方便大家调试理解

// tutorial03.c

// A pedagogical video player that will stream through every video frame as fast as it can

// and play audio (out of sync).

//

// This tutorial was written by Stephen Dranger (dranger@gmail.com).

//

// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard,

// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)

// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1

//

// Updates tested on:

// Mac OS X 10.11.6

// Apple LLVM version 8.0.0 (clang-800.0.38)

//

// Use

//

// $ gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`

//

// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).

//

// Run using

//

// $ tutorial03 myvideofile.mpg

//

// to play the stream on your screen with voice.

/*---------------------------

//1、消息队列处理函数在处理消息前,先对互斥量进行锁定,以保护消息队列中的临界区资源

//2、若消息队列为空,则调用pthread_cond_wait对互斥量暂时解锁,等待其他线程向消息队列中插入消息数据

//3、待其他线程向消息队列中插入消息数据后,通过pthread_cond_signal像等待线程发出qready信号

//4、消息队列处理线程收到qready信号被唤醒,重新获得对消息队列临界区资源的独占

#include <pthread.h>

struct msg{//消息队列结构体

struct msg *m_next;//消息队列后继节点

//more stuff here

}

struct msg *workq;//消息队列指针

pthread_cond_t qready=PTHREAD_COND_INITIALIZER;//消息队列就绪条件变量

pthread_mutex_t qlock=PTHREAS_MUTEX_INITIALIZER;//消息队列互斥量,保护消息队列数据

//消息队列处理函数

void process_msg(void){

struct msg *mp;//消息结构指针

for(;;){

pthread_mutex_lock(&qlock);//消息队列互斥量加锁,保护消息队列数据

while(workq==NULL){//检查消息队列是否为空,若为空

pthread_cond_wait(&qready,&qlock);//等待消息队列就绪信号qready,并对互斥量暂时解锁,该函数返回时,互斥量再次被锁住

}

mp=workq;//线程醒来,从消息队列中取数据准备处理

workq=mp->m_next;//更新消息队列,指针后移清除取出的消息

pthread_mutex_unlock(&qlock);//释放锁

//now process the message mp

}

}

//将消息插入消息队列

void enqueue_msg(struct msg *mp){

pthread_mutex_lock(&qlock);//消息队列互斥量加锁,保护消息队列数据

mp->m_next=workq;//将原队列头作为插入消息的后继节点

workq=mp;//将新消息插入队列

pthread_cond_signal(&qready);//给等待线程发出qready消息,通知消息队列已就绪

pthread_mutex_unlock(&qlock);//释放锁

}

---------------------------*/

#include <libavcodec/avcodec.h>

#include <libavformat/avformat.h>

#include <libswscale/swscale.h>

#include <SDL.h>

#include <SDL_thread.h>

#ifdef __MINGW32__

#undef main // Prevents SDL from overriding main().

#endif

#include <stdio.h>

#define SDL_AUDIO_BUFFER_SIZE 1024

#define MAX_AUDIO_FRAME_SIZE 192000

int quit = 0;//全局退出进程标识,在界面上点了退出后,告诉线程退出

/*-------链表节点结构体--------

typedef struct AVPacketList {

AVPacket pkt;//链表数据

struct AVPacketList *next;//链表后继节点

} AVPacketList;

---------------------------*/

//数据包队列(链表)结构体

typedef struct PacketQueue {

AVPacketList *first_pkt, *last_pkt;//队列首尾节点指针

int nb_packets;//队列长度

int size;//保存编码数据的缓存长度,size=packet->size

SDL_mutex *qlock;//队列互斥量,保护队列数据

SDL_cond *qready;//队列就绪条件变量

} PacketQueue;

PacketQueue audioq;//定义全局队列对象

//队列初始化函数

void packet_queue_init(PacketQueue *q) {

memset(q, 0, sizeof(PacketQueue));//全零初始化队列结构体对象

q->qlock = SDL_CreateMutex();//创建互斥量对象

q->qready = SDL_CreateCond();//创建条件变量对象

}

//向队列中插入数据包

int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

/*-------准备队列(链表)节点对象------*/

AVPacketList *pktlist;//创建链表节点对象指针

pktlist = av_malloc(sizeof(AVPacketList));//在堆上创建链表节点对象

if (!pktlist) {//检查链表节点对象是否创建成功

return -1;

}

pktlist->pkt = *pkt;//将输入数据包赋值给新建链表节点对象中的数据包对象

pktlist->next = NULL;//链表后继指针为空

// if (av_packet_ref(pkt, pkt)<0) {//增加pkt编码数据的引用计数(输入参数中的pkt与新建链表节点中的pkt共享同一缓存空间)

// return -1;

// }

/*---------将新建节点插入队列-------*/

SDL_LockMutex(q->qlock);//队列互斥量加锁,保护队列数据

if (!q->last_pkt) {//检查队列尾节点是否存在(检查队列是否为空)

q->first_pkt = pktlist;//若不存在(队列尾空),则将当前节点作队列为首节点

}

else {

q->last_pkt->next = pktlist;//若已存在尾节点,则将当前节点挂到尾节点的后继指针上,并作为新的尾节点

}

q->last_pkt = pktlist;//将当前节点作为新的尾节点

q->nb_packets++;//队列长度+1

q->size += pktlist->pkt.size;//更新队列编码数据的缓存长度

SDL_CondSignal(q->qready);//给等待线程发出消息,通知队列已就绪

SDL_UnlockMutex(q->qlock);//释放互斥量

return 0;

}

//从队列中提取数据包,并将提取的数据包出队列

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {

AVPacketList *pktlist;//临时链表节点对象指针

int ret;//操作结果

SDL_LockMutex(q->qlock);//队列互斥量加锁,保护队列数据

for (;;) {

if (quit) {//检查退出进程标识

ret = -1;//操作失败

break;

}

pktlist = q->first_pkt;//传递将队列首个数据包指针

if (pktlist) {//检查数据包是否为空(队列是否有数据)

q->first_pkt = pktlist->next;//队列首节点指针后移

if (!q->first_pkt) {//检查首节点的后继节点是否存在

q->last_pkt = NULL;//若不存在,则将尾节点指针置空

}

q->nb_packets--;//队列长度-1

q->size -= pktlist->pkt.size;//更新队列编码数据的缓存长度

*pkt = pktlist->pkt;//将队列首节点数据返回

av_free(pktlist);//清空临时节点数据(清空首节点数据,首节点出队列)

ret = 1;//操作成功

break;

} else if (!block) {

ret = 0;

break;

} else {//队列处于未就绪状态,此时通过SDL_CondWait函数等待qready就绪信号,并暂时对互斥量解锁

/*---------------------

* 等待队列就绪信号qready,并对互斥量暂时解锁

* 此时线程处于阻塞状态,并置于等待条件就绪的线程列表上

* 使得该线程只在临界区资源就绪后才被唤醒,而不至于线程被频繁切换

* 该函数返回时,互斥量再次被锁住,并执行后续操作

--------------------*/

SDL_CondWait(q->qready, q->qlock);//暂时解锁互斥量并将自己阻塞,等待临界区资源就绪(等待SDL_CondSignal发出临界区资源就绪的信号)

}

}//end for for-loop

SDL_UnlockMutex(q->qlock);//释放互斥量

return ret;

}

/*---------------------------

* 从缓存队列中提取数据包、解码,并返回解码后的数据长度(对一个完整的packet解码,将解码数据写入audio_buf缓存,并返回多帧解码数据的总长度)

* aCodecCtx:音频解码器上下文

* audio_buf:保存解码一个完整的packe后的原始音频数据(缓存中可能包含多帧解码后的音频数据)

* buf_size:解码后的音频数据长度,未使用

--------------------------*/

int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) {

static AVPacket pkt;//保存从队列中提取的数据包

static AVFrame frame;//保存从数据包中解码的音频数据

static uint8_t *audio_pkt_data = NULL;//保存数据包编码数据缓存指针

static int audio_pkt_size = 0;//数据包中剩余的编码数据长度

int coded_consumed_size, data_size = 0;//每次消耗的编码数据长度[input](len1),输出原始音频数据的缓存长度[output]

for (;;) {

while(audio_pkt_size>0) {//检查缓存中剩余的编码数据长度(是否已完成一个完整的pakcet包的解码,一个数据包中可能包含多个音频编码帧)

int got_frame = 0;//解码操作成功标识,成功返回非零值

coded_consumed_size=avcodec_decode_audio4(aCodecCtx,&frame,&got_frame,&pkt);//解码一帧音频数据,并返回消耗的编码数据长度

if (coded_consumed_size < 0) {//检查是否执行了解码操作

// if error, skip frame.

audio_pkt_size = 0;//更新编码数据缓存长度

break;

}

audio_pkt_data += coded_consumed_size;//更新编码数据缓存指针位置

audio_pkt_size -= coded_consumed_size;//更新缓存中剩余的编码数据长度

if (got_frame) {//检查解码操作是否成功

//计算解码后音频数据长度[output]

data_size=av_samples_get_buffer_size(NULL,aCodecCtx->channels,frame.nb_samples,aCodecCtx->sample_fmt,1);

memcpy(audio_buf, frame.data[0], data_size);//将解码数据复制到输出缓存

}

if (data_size <= 0) {//检查输出解码数据缓存长度

// No data yet, get more frames.

continue;

}

// We have data, return it and come back for more later.

return data_size;//返回解码数据缓存长度

}//end for while

if (pkt.data) {//检查数据包是否已从队列中提取

av_packet_unref(&pkt);//释放pkt中保存的编码数据

}

if (quit) {//检查退出进程标识

return -1;

}

//从队列中提取数据包到pkt

if (packet_queue_get(&audioq, &pkt,1)<0) {

return -1;

}

audio_pkt_data = pkt.data;//传递编码数据缓存指针

audio_pkt_size = pkt.size;//传递编码数据缓存长度

}//end for for-loop

}

/*------Audio Callback-------

* 音频输出回调函数,sdl通过该回调函数将解码后的pcm数据送入声卡播放,

* sdl通常一次会准备一组缓存pcm数据,通过该回调送入声卡,声卡根据音频pts依次播放pcm数据

* 待送入缓存的pcm数据完成播放后,再载入一组新的pcm缓存数据(每次音频输出缓存为空时,sdl就调用此函数填充音频输出缓存,并送入声卡播放)

* When we begin playing audio, SDL will continually call this callback function

* and ask it to fill the audio buffer with a certain number of bytes

* The audio function callback takes the following parameters:

* stream: A pointer to the audio buffer to be filled,输出音频数据到声卡缓存

* len: The length (in bytes) of the audio buffer,缓存长度wanted_spec.samples=SDL_AUDIO_BUFFER_SIZE(1024)

--------------------------*/

void audio_callback(void *userdata, Uint8 *stream, int len) {

AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;//传递用户数据

int wt_stream_len, audio_size;//每次写入stream的数据长度,解码后的数据长度

static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];//保存解码一个packet后的多帧原始音频数据

static unsigned int audio_buf_size = 0;//解码后的多帧音频数据长度

static unsigned int audio_buf_index = 0;//累计写入stream的长度

while (len>0) {//检查音频缓存的剩余长度

if (audio_buf_index >= audio_buf_size) {//检查是否需要执行解码操作

// We have already sent all our data; get more,从缓存队列中提取数据包、解码,并返回解码后的数据长度,audio_buf缓存中可能包含多帧解码后的音频数据

audio_size = audio_decode_frame(aCodecCtx, audio_buf, audio_buf_size);

if (audio_size < 0) {//检查解码操作是否成功

// If error, output silence.

audio_buf_size = 1024; // arbitrary?

memset(audio_buf, 0, audio_buf_size);//全零重置缓冲区

} else {

audio_buf_size = audio_size;//返回packet中包含的原始音频数据长度(多帧)

}

audio_buf_index = 0;//初始化累计写入缓存长度

}//end for if

wt_stream_len = audio_buf_size-audio_buf_index;//计算解码缓存剩余长度

if (wt_stream_len > len) {//检查每次写入缓存的数据长度是否超过指定长度(1024)

wt_stream_len = len;//指定长度从解码的缓存中取数据

}

//每次从解码的缓存数据中以指定长度抽取数据并写入stream传递给声卡

memcpy(stream,(uint8_t*)audio_buf+audio_buf_index,wt_stream_len);

len -= wt_stream_len;//更新解码音频缓存的剩余长度

stream += wt_stream_len;//更新缓存写入位置

audio_buf_index += wt_stream_len;//更新累计写入缓存数据长度

}//end for while

}

int main(int argc, char *argv[]) {

/*--------------参数定义-------------*/

AVFormatContext *pFormatCtx = NULL;//保存文件容器封装信息及码流参数的结构体

AVCodecContext *vCodecCtx = NULL;//视频解码器上下文对象,解码器依赖的相关环境、状态、资源以及参数集的接口指针

AVCodecContext *aCodecCtx = NULL;//音频解码器上下文对象,解码器依赖的相关环境、状态、资源以及参数集的接口指针

AVCodec *vCodec = NULL;//保存视频编解码器信息的结构体,提供编码与解码的公共接口,可以看作是编码器与解码器的一个全局变量

AVCodec *aCodec = NULL;//保存音频编解码器信息的结构体,提供编码与解码的公共接口,可以看作是编码器与解码器的一个全局变量

AVPacket packet;//负责保存压缩编码数据相关信息的结构体,每帧图像由一到多个packet包组成

AVFrame *pFrame = NULL;//保存音视频解码后的数据,如状态信息、编解码器信息、宏块类型表,QP表,运动矢量表等数据

struct SwsContext *sws_ctx = NULL;//描述转换器参数的结构体

AVDictionary *videoOptionsDict = NULL;

AVDictionary *audioOptionsDict = NULL;

SDL_Surface *screen = NULL;//SDL绘图表面,A structure that contains a collection of pixels used in software blitting

SDL_Overlay *bmp = NULL;//SDL画布

SDL_Rect rect;//SDL矩形对象

SDL_AudioSpec wanted_spec, spec;//SDL_AudioSpec a structure that contains the audio output format,创建 SDL_AudioSpec 结构体,设置音频播放数据

SDL_Event event;//SDL事件对象

int i, videoStream, audioStream;//循环变量,音视频流类型标号

int frameFinished;//解码操作是否成功标识

/*-------------参数初始化------------*/

if (argc<2) {//检查输入参数个数是否正确

fprintf(stderr, "Usage: test <file>\n");

exit(1);

}

// Register all formats and codecs,注册所有多媒体格式及编解码器

av_register_all();

// Open video file,打开视频文件,取得文件容器的封装信息及码流参数

if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {

return -1; // Couldn't open file.

}

// Retrieve stream information,取得文件中保存的码流信息

if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {

return -1; // Couldn't find stream information.

}

// Dump information about file onto standard error,打印pFormatCtx中的码流信息

av_dump_format(pFormatCtx, 0, argv[1], 0);

// Find the first video stream.

videoStream = -1;//视频流类型标号初始化为-1

audioStream = -1;//音频流类型标号初始化为-1

for (i = 0; i < pFormatCtx->nb_streams; i++) {//遍历文件中包含的所有流媒体类型(视频流、音频流、字幕流等)

if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && videoStream < 0) {//若文件中包含有视频流

videoStream = i;//用视频流类型的标号修改标识,使之不为-1

}

if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audioStream < 0) {//若文件中包含有音频流

audioStream = i;//用音频流类型的标号修改标识,使之不为-1

}

}

if (videoStream == -1) {//检查文件中是否存在视频流

return -1; // Didn't find a video stream.

}

if (audioStream == -1) {//检查文件中是否存在音频流

return -1;

}

// Get a pointer to the codec context for the video stream,根据流类型标号从pFormatCtx->streams中取得视频流对应的解码器上下文

vCodecCtx = pFormatCtx->streams[videoStream]->codec;

/*-----------------------

* Find the decoder for the video stream,根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)

* The stream's information about the codec is in what we call the "codec context.

* This contains all the information about the codec that the stream is using

-----------------------*/

vCodec = avcodec_find_decoder(vCodecCtx->codec_id);

if (vCodec == NULL) {//检查解码器是否匹配

fprintf(stderr, "Unsupported codec!\n");

return -1; // Codec not found.

}

if (avcodec_open2(vCodecCtx, vCodec, &videoOptionsDict) < 0)// Open codec,打开视频解码器

return -1; // Could not open codec.

// Get a pointer to the codec context for the video stream,根据流类型标号从pFormatCtx->streams中取得音频流对应的解码器上下文

aCodecCtx = pFormatCtx->streams[audioStream]->codec;

// Find the decoder for the video stream,根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)

aCodec = avcodec_find_decoder(aCodecCtx->codec_id);

if (!aCodec) {//检查解码器是否匹配

fprintf(stderr, "Unsupported codec!\n");

return -1;

}

avcodec_open2(aCodecCtx, aCodec, &audioOptionsDict);// Open codec,打开音频解码器

// Allocate video frame,为解码后的视频信息结构体分配空间并完成初始化操作(结构体中的图像缓存按照下面两步手动安装)

pFrame = av_frame_alloc();

// Initialize SWS context for software scaling,设置图像转换像素格式为AV_PIX_FMT_YUV420P

sws_ctx = sws_getContext(vCodecCtx->width, vCodecCtx->height, vCodecCtx->pix_fmt, vCodecCtx->width, vCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);

packet_queue_init(&audioq);//缓存队列初始化

//SDL_Init initialize the Event Handling, File I/O, and Threading subsystems,初始化SDL

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {//initialize the video audio & timer subsystem

fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());

exit(1);

}

// Make a screen to put our video,在SDL2.0中SDL_SetVideoMode及SDL_Overlay已经弃用,改为SDL_CreateWindow及SDL_CreateRenderer创建窗口及着色器

#ifndef __DARWIN__

screen = SDL_SetVideoMode(vCodecCtx->width, vCodecCtx->height, 0, 0);//创建SDL窗口及绘图表面,并指定图像尺寸及像素个数

#else

screen = SDL_SetVideoMode(vCodecCtx->width, vCodecCtx->height, 24, 0);//创建SDL窗口及绘图表面,并指定图像尺寸及像素个数

#endif

if (!screen) {//检查SDL(绘图表面)窗口是否创建成功(SDL用绘图表面对象操作窗口)

fprintf(stderr, "SDL: could not set video mode - exiting\n");

exit(1);

}

SDL_WM_SetCaption(argv[1],0);//用输入文件名设置SDL窗口标题

// Allocate a place to put our YUV image on that screen,创建画布对象

bmp = SDL_CreateYUVOverlay(vCodecCtx->width, vCodecCtx->height, SDL_YV12_OVERLAY, screen);

// Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format

// 创建SDL_AudioSpec结构体,设置音频播放参数

wanted_spec.freq = aCodecCtx->sample_rate;//采样频率 DSP frequency -- samples per second

wanted_spec.format = AUDIO_S16SYS;//采样格式 Audio data format

wanted_spec.channels = aCodecCtx->channels;//声道数 Number of channels: 1 mono, 2 stereo

wanted_spec.silence = 0;//无输出时是否静音

wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;//默认每次读音频缓存的大小,推荐值为 512~8192,ffplay使用的是1024 specifies a unit of audio data refers to the size of the audio buffer in sample frames

wanted_spec.callback = audio_callback;//设置取音频数据的回调接口函数 the function to call when the audio device needs more data

wanted_spec.userdata = aCodecCtx;//传递用户数据

/*---------------------------

* 以指定参数打开音频设备,并返回与指定参数最为接近的参数,该参数为设备实际支持的音频参数

* Opens the audio device with the desired parameters(wanted_spec)

* return another specs we actually be using

* and not guaranteed to get what we asked for

--------------------------*/

if (SDL_OpenAudio(&wanted_spec, &spec)<0) {

fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());

return -1;

}

SDL_PauseAudio(0);//audio callback starts running again,开启音频设备,如果这时候没有获得数据那么它就静音

/*--------------循环解码-------------*/

i = 0;// Read frames and save first five frames to disk.

/*-----------------------

* read in a packet and store it in the AVPacket struct

* ffmpeg allocates the internal data for us,which is pointed to by packet.data

* this is freed by the av_free_packet()

-----------------------*/

while (av_read_frame(pFormatCtx, &packet) >= 0) {//从文件中依次读取每个图像编码数据包,并存储在AVPacket数据结构中

// Is this a packet from the video stream,检查数据包类型

if (packet.stream_index == videoStream) {//检查视频媒体流类型标识

/*-----------------------

* Decode video frame,解码完整的一帧数据,并将frameFinished设置为true

* 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行

* avcodec_decode_video2()会在解码到完整的一帧时设置frameFinished为真

* Technically a packet can contain partial frames or other bits of data

* ffmpeg's parser ensures that the packets we get contain either complete or multiple frames

* convert the packet to a frame for us and set frameFinisned for us when we have the next frame

-----------------------*/

avcodec_decode_video2(vCodecCtx, pFrame, &frameFinished, &packet);

// Did we get a video frame,检查是否解码出完整一帧图像

if (frameFinished) {

SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data,原子操作,保护像素缓冲区,避免非法修改

AVFrame pict;//保存转换为AV_PIX_FMT_YUV420P格式的视频帧

pict.data[0] = bmp->pixels[0];//将转码后的图像与画布的像素缓冲器关联

pict.data[1] = bmp->pixels[2];

pict.data[2] = bmp->pixels[1];

pict.linesize[0] = bmp->pitches[0];//将转码后的图像扫描行长度与画布像素缓冲区的扫描行长度相关联

pict.linesize[1] = bmp->pitches[2];//linesize-Size, in bytes, of the data for each picture/channel plane

pict.linesize[2] = bmp->pitches[1];;//For audio, only linesize[0] may be set

// Convert the image into YUV format that SDL uses,将解码后的图像转换为AV_PIX_FMT_YUV420P格式,并赋值到pict对象

sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, vCodecCtx->height, pict.data, pict.linesize);

SDL_UnlockYUVOverlay(bmp);//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed

//设置矩形显示区域

rect.x = 0;

rect.y = 0;

rect.w = vCodecCtx->width;

rect.h = vCodecCtx->height;

SDL_DisplayYUVOverlay(bmp, &rect);//图像渲染

av_packet_unref(&packet);//Free the packet that was allocated by av_read_frame,释放AVPacket数据结构中编码数据指针

}

} else if (packet.stream_index == audioStream) {//检查音频媒体流类型标识

packet_queue_put(&audioq, &packet);//向缓存队列中填充编码数据包

} else {//字幕流类型标识

//Free the packet that was allocated by av_read_frame,释放AVPacket数据结构中编码数据指针

av_packet_unref(&packet);

}

/*-------------------------

* 在每次循环中从SDL后台队列取事件并填充到SDL_Event对象中

* SDL的事件系统使得你可以接收用户的输入,从而完成一些控制操作

* SDL_PollEvent() is the favored way of receiving system events

* since it can be done from the main loop and does not suspend the main loop

* while waiting on an event to be posted

* poll for events right after we finish processing a packet

------------------------*/

SDL_PollEvent(&event);

switch (event.type) {//检查SDL事件对象

case SDL_QUIT://退出事件

quit = 1;//退出进程标识置1

SDL_Quit();//退出操作

exit(0);//结束进程

break;

default:

break;

}//end for switch

}//end for while

/*--------------参数撤销-------------*/

// Free the YUV frame.

av_free(pFrame);

// Close the codec.

avcodec_close(vCodecCtx);

// Close the video file.

avformat_close_input(&pFormatCtx);

return 0;

}

原文 https://zhuanlan.zhihu.com/p/532867847

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ffmpeg播放器实现详解 - 音频播放 的相关文章

  • 使用 Ffmpeg 编辑视频元数据

    我想更改视频元数据 原始视频信息 ffmpeg i video mp4 Metadata major brand mp42 minor version 0 compatible brands isomavc1mp42 creation ti
  • Qt WinRT 应用程序无法访问文件权限被拒绝

    我需要使用 Qt 和 FFMPEG 开发 WinRT 应用程序 我根据指令构建了 WinRT 的 ffmpeghere https github com Microsoft FFmpegInterop我可以将库与我的项目链接起来 现在我需要
  • ffmpeg:音频样本的字节顺序

    我使用 ffmpeg 的 avcodec 从我的 C 应用程序中的音乐文件中检索原始音频样本 对于我测试的文件 这些文件样本的字节序似乎是小字节序 但我想知道对于我尝试解码的所有文件是否总是如此 即来自 ffmpeg 的实现或至少它的体系结
  • 在 MacOS 终端上运行 ffmpeg [关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我对 MacOS 相当陌生 我发现使用终端来获取信息并不容易ffmpeg和我在 Window 上一样正常运行 我有 ffmpeg 二进制文件ffmpe
  • FFMPEG:使用具有复杂滤波器的视频滤波器

    我正在使用fluent ffmpeg用于对视频文件执行批量操作的 Node js 库 视频过滤器裁剪 16 9 输入 添加填充并将字幕刻录到填充中 在下一步中 我想使用复杂的滤镜来覆盖图像作为水印 ff input video mp4 ff
  • 为什么处理时间随着修剪位置的增加而增加?

    我最近一直在使用ffmpeg修剪一些视频 我注意到随着修剪位置的增加 修剪视频所花费的时间也会增加 即使持续时间相同 5 seconds 下面给出了修剪视频的命令0 to 5秒 处理仅需1秒 ffmpeg y i input mp4 fil
  • 如何将 MP3 音频文件读入 numpy 数组/将 numpy 数组保存到 MP3?

    有没有办法从 MP3 音频文件中读取 写入 MP3 音频文件numpy具有类似 API 的数组scipy io wavfile read https docs scipy org doc scipy 0 14 0 reference gen
  • Ffmpeg 无法正确转换为 ogg [关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我正在使用 ffmpeg 在我的网站上转换音频和视频 Ffmpeg 可以正确转换为其他格式 如 mp3 mp4 等 但无法正确转换为 ogg 虽然 f
  • 使用 Coldfusion 的 CFFILE 标签监控 FFMpeg 的进度日志

    我想学习如何使用 ColdFusion 中的 CFFILE 标签来读取文本文件的内容 就我而言 该文本文件是 FFMpeg 在对媒体文件进行转码时生成的进度日志 我想编写一个 ColdFusion 脚本 该脚本将定期轮询进度日志 直到日志表
  • Node.js - 将数据缓冲到 Ffmpeg

    我使用 Node js 和 Ffmpeg 来创建动画 因为我试图避免第三方 avi mp4 解析器 所以我决定将动画输出为原始 rgb24 数据文件 然后使用一些程序将其转换为 mp4 文件 我发现 Ffmpeg 是免费且开源的 它完全可以
  • Xuggler 未转换 .webm 文件?

    我只是尝试使用 Xuggler 将 mov 文件转换为 webm 这应该可以工作 因为 FFMPEG 支持 webm 文件 这是我的代码 IMediaReader reader ToolFactory makeReader home use
  • Android 中的 FFMpeg jni?

    我已经构建了 Bambuser http bambuser com opensource 提供的 FFMPEG 可执行文件和库 所以我设法构建了 Android 可执行文件和库 如何在 Eclipse 项目中链接这些库并从 Java 调用
  • id3 图像编辑后播放 mp3 时遇到问题

    由于硬件限制 我们生产的软件试图确保导入到其库中的任何音频文件 准备复制到硬件上 都是可接受的比特率 最近 我们开始使用 FFmpeg 将许多不同的音频类型转换为 mp3 以便在我们的硬件上导入和使用它们 虽然转换工作正常并且 mp3 文件
  • ffmpeg计算视频比特率

    我想知道如何计算该视频的比特率 http jell yfish us media jellyfish 30 mbps hd hevc mkv http jell yfish us media jellyfish 30 mbps hd hev
  • swscaler 警告:使用了已弃用的像素格式

    我想先对视频帧执行颜色空间转换 然后使用以下代码将其转换为 opengl 纹理 struct SwsContext pSwsCtx sws getCachedContext NULL width height codec gt pix fm
  • 使用 ffmpeg 将 mp4 转换为 gif 时帧率较低

    我正在使用 ffmpeg 将高质量视频转换为 gif 大多数视频都是 60fps 及以上720p 但是当我使用下面的代码将视频转换为 gif 时 我得到的 gif 输出的 fps 非常低 usr bin env palette tmp pa
  • 有没有更有效的方法通过ffmpeg批量添加水印和加入视频?

    我有这个批处理文件 使用 ffmpeg 在我的视频中添加徽标 然后添加简介 但需要 10 小时到一天的时间 具体取决于我需要添加水印的数量 是否有更有效的方法来实现此目的 视频有时具有不同的分辨率 因此我无法删除到 1280 720 尺寸的
  • ffmpeg 命令行,用于使用 Windows 7 从 Decklink 卡捕获(和录制)720p 音频和视频

    我正在尝试使用 Windows 7 720p 从 blackmagic Decklink 捕获卡捕获音频和视频 但我似乎无法正确获取 ffmpeg 命令行设置 ffmpeg list devices true f dshow i 虚拟 ds
  • 用于 Windows Phone 开发的 FFmpeg

    我在 ASP Net 基于 Web 的应用程序中使用了 FFmpeg 现在我想用它来进行Windows Phone开发 可以使用吗 如果是 那么如何 Windows Phone 7 根本不支持 FFmpeg 而且据我在网上找到的信息 Win
  • ffmpeg视频已压缩但无法在浏览器中播放

    我已经集成了ffmpeg4android lib 视频压缩工作正常 但视频无法在除 safari 浏览器之外的浏览器中播放 上传到服务器后 我使用了以下命令 ffmpeg y i

随机推荐

  • Q老师的考验【矩阵快速幂】【斐波那契数列】

    问题描述 Q老师 对数列有一种非同一般的热爱 xff0c 尤其是优美的斐波那契数列 这一天 xff0c Q老师 为了增强大家对于斐波那契数列的理解 xff0c 决定在斐波那契的基础上创建一个新的数列 f x 来考一考大家 数列 f x 定义
  • Q老师度假【动态规划dp】【矩阵快速幂优化】

    问题描述 忙碌了一个学期的 Q老师 决定奖励自己 N 天假期 假期中不同的穿衣方式会有不同的快乐值 已知 Q老师 一共有 M 件衬衫 xff0c 且如果昨天穿的是衬衫 A xff0c 今天穿的是衬衫 B xff0c 则 Q老师 今天可以获得
  • 插值算法[数学建模]

    插值 插值算法一维数据插值方法分段线性插值分段二次插值 xff08 分段抛物插值 xff09 拉格朗日插值法龙格现象 牛顿插值法埃尔米特插值法分段三次埃尔米特插值PCHIP 样条插值三次样条插值 n维数据插值应用 插值算法 在工程和数学应用
  • 时间序列【数学建模】

    时间序列 确定性时间序列分析方法移动平均法简单移动平均法加权移动平均法趋势移动平均法 指数平滑法一次指数平滑法二次指数平滑法三次指数平滑法 差分指数平滑法一阶差分指数平滑法二阶差分指数平滑法 具有季节性特点的时间序列的预测 平稳时间序列模型
  • UITabBarController的简单使用和属性方法总结

    一 引言 与导航控制器相类似 xff0c 标签控制器也是用于管理视图控制器的一个UI控件 xff0c 在其内部封装了一个标签栏 xff0c 与导航不同的是 xff0c 导航的管理方式是纵向的 xff0c 采用push与pop切换控制器 xf
  • 多元分析——聚类分析【数学建模】

    聚类分析 Q型聚类分析样本的相似性度量类与类间的相似性度量聚类图最短距离法的聚类举例Matlab 聚类分析的相关命令pdistlinkageclusterzsore X H 61 dendrogram Z P T 61 clusterdat
  • 多元分析——主成分分析【数学建模】

    主成分分析的主要目的是希望用较少的变量去解释原来资料中的大部分变异 xff0c 将许多相关性较高的变量转化为彼此相互独立或不相关的变量 通常是选出的比原始变量个数少 xff0c 能解释大部份资料中的变异的几个新变量 xff0c 即所谓主成分
  • Pytorch Resnet101

    Pytorch Resnet101 模型使用模板 模型 span class token string 34 34 span 34 model span class token operator span span class token
  • t-SNE可视化

    t SNE的全称是t Distributed Stochastic Neighbor Embedding xff0c 是一种降低维度的技术 xff0c 适用于将高维度数据可视化 它将数据点之间的相似性转换为联合概率 xff0c 并试图最小化
  • ubuntu下为apt-get设置代理

    debian mint都可以按此设置 xff08 debian系均可 xff0c 直接基于debian或间接基于debian的 xff09 现在公司很多都是通过代理上网的 xff0c 通过代理上网apt get install无法通网络进行
  • 开源虚拟化集群管理平台ProxmoxVE-安装介绍以及PCI直通和嵌套虚拟化说明

    说明 xff1a 当前测试环境为PVE7 0版本 官网 xff1a Proxmox Powerful open source server solutions 目录 背景 安装系统 PCI直通配置 xff08 开启iommu xff09 允
  • qt Linux arm 交叉编译

    1 操作系统 ubuntu1 20 04 2 qt5 9 9 Index of archive qt 5 9 5 9 9 下载qt源码 qt everywhere opensource src 5 9 9 tar xz 和程序 qt ope
  • 三角函数:图像和性质关系

    紧接上一篇 xff1a http blog csdn net yinhun2012 article details 79377728 这次我们通过函数图文和unity程序来观察三角函数的图像 xff0c 首先从基本的来 xff1a 1 f
  • Linux 每天定时关机 设置

    sudo gedit etc crontab 输入密码后 xff0c 在该文件插入一行 50 02 root sbin shutdown h now 保存后 xff0c 系统会在每天02 50 自动关机
  • anaconda如何配置环境变量

    anaconda安装好后 xff0c 在cmd输入conda xff0c 显示 xff1a conda 不是内部或外部命令 xff0c 也不是可运行的程序 或批处理文件 原因是 xff1a anaconda没有配置环境变量 那接下来我就教大
  • Windows下用 Code blocks + mingw 搭建 Fortran 编译环境

    方法一 xff1a 下载页面 xff1a http www codeblocks org downloads binaries 下载 codeblocks 17 12mingw fortran setup exe 这样的版本 方法二 xff
  • 音频编码格式介绍-AAC

    目录 概述 帧格式 算法简介 开源的软件 AAC和MP3的关键性不同 参考阅读 1 概述 AAC xff08 Advanced Audio Coding xff09 xff0c 被认为是MP3的继任者 xff0c 相对MP3有更高的压缩效率
  • FFmpeg入门 - 视频播放

    音视频最好从能够直接看到东西 也更加贴近用户的播放开始学起 音视频编解码基础 我们可以通过http rtmp或者本地的视频文件去播放视频 这里的 34 视频 34 实际上指的是mp4 avi这种既有音频也有视频的文件格式 这样的视频文件可能
  • FFmpeg入门 - 格式转换

    1 音频分 plane 与打包 packed 解码出来的AVFrame 它的data字段放的是视频像素数据或者音频的PCM裸流数据 linesize字段放的是对齐后的画面行长度或者音频的分片长度 For video size in byte
  • ffmpeg播放器实现详解 - 音频播放

    1 生产者 消费者线程模型 本文主要讨论posix标准下的生产者 消费者线程模型 xff0c posix标准多用于类linux相关环境 POSIX The Portable Operating System Interface POSIX