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

在上一篇文章中介绍了如果将ffmpeg解码出的视频帧进行渲染显示

本文在上篇文章的基础上,讨论如何将ffmpeg解码出的音频帧进行播放

在开始音频播放问题正式讨论前,我们先引入一个经典的生产者-消费者线程同步模型,用于描述与音视频帧队列,或音视频编码数据包队列相关线程的同步过程

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值