隔了好久好久。。。。,其实ffmpeg是有专门的教程的,只是其中很多内容过期了,我这里也只是当作一个学习的过程,然后记录下来。音视频开发需要具备的知识点如下(从他人处copy过来的)
- rtsp、sdp、tcp、udp、ip协议(rtsp的DESCRIBE、OPTION、SETUP、PLAY、PAUSE、TEARDOWN;tcp连接的三次握手/断开的四次握手)
- socket
- 多线程
- opengl es
- FFmpeg
- YUV420(知道它的原理与格式)
- 音视频同步(时间戳的处理)
- C语言指针
言归正传:
音频播放的基本流程
基本流程跟上一节的视频播放其实差不多,但是这里由于采用SDL的音频播放机制,所以需要采用多线程的方式调整一下基本流程(之后也需要对视频播放进行改造),基本流程其实就是生产者消费者模式,一个线程不停的从某个队列中读数据,读不到就等着,另外一个线程不停的往某个队列中塞数据。
创建队列的代码:
typedef struct PacketQueue{
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
}PacketQueue;
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pktl;
if (av_packet_ref(pkt, pkt) < 0){
return -1;
}
pktl = (AVPacketList *)av_malloc(sizeof(AVPacketList));
if (!pktl) {
return -1;
}
pktl->pkt = *pkt;
SDL_LockMutex(q->mutex);//生产者消费者模式互斥锁
//指针操作,把最新生成的packet插入队尾,保证每次取的packet都是按照解码顺序的
if (!q->last_pkt) {
q->first_pkt = pktl;
} else {
q->last_pkt->next = pktl;
}
q->last_pkt = pktl;
q->nb_packets ++ ;
q->size += pktl->pkt.size;
//条件锁释放有资源的信号
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pktl;
int ret;
SDL_LockMutex(q->mutex);
do{
if (quit) {
ret = -1;
break;
}
pktl = q->first_pkt;
if (pktl) {
//指针操作,从队列中把队首元素取出来
q->first_pkt = pktl->next;
if (!q->first_pkt) {
q->last_pkt = NULL;
}
q->nb_packets--;
q->size -= pktl->pkt.size;
*pkt = pktl->pkt;
av_free(pktl);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
//这个条件锁,能在条件不达要求的时候,把互斥锁释放
SDL_CondWait(q->cond, q->mutex);
}
}while(true);
SDL_UnlockMutex(q->mutex);
return ret;
}
生产者线程流程
- 注册需要支持的文件格式以及对应的编解码器
- 打开文件
- 从文件中提取数据流信息
- 从数据流(视频数据流/音频数据流/字幕数据流)中找到对应的音频数据流
- 从音频数据流中找到对应的解码器
- 打开解码器
- 将原始帧塞入队列中以供消费
int main(int argc, char *argv[]) {
SDL_Event event;
AVFormatContext* formatContext = avformat_alloc_context();//初始化AVFormatContext
AVCodecContext* codeContext = NULL;
int audioIndex = -1;
av_register_all();//一劳永逸,第一步注册所有的文件格式以及编码器 deprecated
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);//初始化SDL
NSString * path = [[NSBundle mainBundle]pathForResource:@"test.mp3" ofType:@""];//获取文件路径
if(0!=avformat_open_input(&formatContext, [path UTF8String], NULL, NULL)){
return -1;//第二步打开文件,并存入formatContext中。
}
if(0!=avformat_find_stream_info(formatContext, NULL)){
return -1;//第三步从formatContext对象中读取AVStream信息.
}
AVCodec* codec = NULL;
AVPacket packet;
for(int i = 0;i<formatContext->nb_streams;i++){
AVStream * stream = formatContext->streams[i];
if(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){//第四步遍历formatContext中的所有数据流。从中选出音频流
codec = avcodec_find_decoder(stream->codecpar->codec_id);//第五步,根据流的信息找到对应的解码器。
codeContext = stream->codec;
audioIndex = i;
break;
}
}
if(codeContext == NULL)
return -1;
SDL_AudioSpec wanted_spec, spec;
wanted_spec.freq = codeContext->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = codeContext->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;//音频硬件从此方法中获取相应的数据播放
wanted_spec.userdata = codeContext;
//使用SDL音频播放功能
if(SDL_OpenAudio(&wanted_spec, &spec) < 0)
{
fprintf(stderr, "SDL_OpenAudio: %s \n", SDL_GetError());
return -1;
}
//第六步,打开解码器
avcodec_open2(codeContext, codec, NULL);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
if(0!=avcodec_open2(codeContext, codec, NULL))//第六步,打开编解码器
return -1;
while (av_read_frame(formatContext, &packet) >= 0)
{
//读取压缩帧塞入队列中
if (packet.stream_index == audioIndex)
packet_queue_put(&audioq, &packet);
else
av_free_packet(&packet);
SDL_PollEvent(&event);
switch (event.type) {
case SDL_QUIT:
quit = 1;
SDL_Quit();
exit(0);
break;
default:
break;
}
}
//
// 在控制台输出文件信息
av_dump_format(formatContext, 0, [[[NSBundle mainBundle] pathForResource:@"test.mp3" ofType:@""] UTF8String], 0);
return 0;
}
消费者线程流程
- 音频硬件利用回调函数,获取数据,填满自己的缓冲区
- 从队列中获取压缩帧
- 获取解压缩帧
- 解码数据然后放入SDL播放音频
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t * audio_buf, int buf_size) {
static AVPacket thisPkt;
static AVFrame frame;
int data_size = 0;
do{
//需要先使用avcodec_send_packet发送了压缩帧才能从aCodecCtx中获取解压帧
while (0 == avcodec_receive_frame(aCodecCtx, &frame)) {
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){
continue;
}
printf("saving frame %3d\n", aCodecCtx->frame_number);
return data_size;
}
if (thisPkt.data) {
av_packet_unref(&thisPkt);
}
if (quit) {
return -1;
}
if (packet_queue_get(&audioq, &thisPkt, 1) < 0) {
return -1;
} else {
avcodec_send_packet(aCodecCtx, &thisPkt);
}
}while(true);
}
void audio_callback(void *userdata, uint8_t *stream, int len) {
//外部传进来的userdata,stream可以理解为SDL音频硬件的缓冲区,len表示缓冲区大小
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int lenl ,audio_size;
//audio_buf用于暂存解压帧数据的缓冲区,最后要把这个里面的数据copy到stream中,直到这个缓冲区被填满。
static uint8_t audio_buf[(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_size = audio_decode_frame(aCodecCtx, audio_buf, audio_buf_size);
if (audio_size < 0 ) {
audio_buf_size = 1024;
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
lenl = audio_buf_size - audio_buf_index;
if (lenl > len) {
lenl = len;
}
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, lenl);
len -= lenl;
stream += lenl;
audio_buf_index += lenl;
}
}
以上代码还有一个缺点,就是基本是杂音。需要重采样(还是有杂音,具体原因我暂时也不知道),重采样的代码在下面的链接中:
如果觉得我的文章对你有帮助,希望能点个赞,您的“点赞”将是我最大的写作动力,如果觉得有什么问题也可以直接指出。