意外情况
你们将会注意到我们有一个全局变量quit,我们用它来保证还没有设置程序退出的信号(SDL会自动处理TERM类似的信号)。否则,这个线程将不停地运 行直到我们使用kill -9来结束程序。FFMPEG同样也提供了一个函数来进行回调并检查我们是否需要退出一些被阻塞的函数:这个函数就是 url_set_interrupt_cb。
1 int decode_interrupt_cb(void) { 2 return quit; 3 } 4 ... 5 main() { 6 ... 7 url_set_interrupt_cb(decode_interrupt_cb); 8 ... 9 SDL_PollEvent(&event); 10 switch(event.type) { 11 case SDL_QUIT: 12 quit = 1; 13 ...
当然,这仅仅是用来给ffmpeg中的阻塞情况使用的,而不是SDL中的。我们还必需要设置quit标志为1。
为队列提供包
剩下的我们唯一需要为队列所做的事就是提供包了:
1 PacketQueue audioq; 2 main() { 3 ... 4 avcodec_open(aCodecCtx, aCodec); 5 packet_queue_init(&audioq); 6 SDL_PauseAudio(0);
函数SDL_PauseAudio()让音频设备最终开始工作。如果没有立即供给足够的数据,它会播放静音。
我们已经建立好我们的队列,现在我们准备为它提供包。先看一下我们的读取包的循环:
1 while(av_read_frame(pFormatCtx, &packet)>=0) { 2 // Is this a packet from the video stream? 3 if(packet.stream_index==videoStream) { 4 // Decode video frame 5 .... 6 } 7 } else if(packet.stream_index==audioStream) { 8 packet_queue_put(&audioq, &packet); 9 } else { 10 av_free_packet(&packet); 11 }
注意:我们没有在把包放到队列里的时候释放它,我们将在解码后来释放它。
取出包
现在,让我们最后让声音回调函数audio_callback来从队列中取出包。回调函数的格式必需为void callback(void *userdata, Uint8 *stream, int len),这里的userdata就是我们给到SDL的指针,stream是我们要把声音数据写入的缓冲区指针,len是缓冲区的大小。下面就是代码:
1 void audio_callback(void *userdata, Uint8 *stream, int len) { 2 AVCodecContext *aCodecCtx = (AVCodecContext *)userdata; 3 int len1, audio_size; 4 static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2]; 5 static unsigned int audio_buf_size = 0; 6 static unsigned int audio_buf_index = 0; 7 while(len > 0) { 8 if(audio_buf_index >= audio_buf_size) { 9 audio_size = audio_decode_frame(aCodecCtx, audio_buf, 10 sizeof(audio_buf)); 11 if(audio_size < 0) { 12 audio_buf_size = 1024; 13 memset(audio_buf, 0, audio_buf_size); 14 } else { 15 audio_buf_size = audio_size; 16 } 17 audio_buf_index = 0; 18 } 19 len1 = audio_buf_size - audio_buf_index; 20 if(len1 > len) 21 len1 = len; 22 memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1); 23 len -= len1; 24 stream += len1; 25 audio_buf_index += len1; 26 } 27 }
这基本上是一个简单的从另外一个我们将要写的audio_decode_frame()函数中获取数据的循环,这个循环把结果写入到中间缓冲区,尝试着向 流中写入len字节并且在我们没有足够的数据的时候会获取更多的数据或者当我们有多余数据的时候保存下来为后面使用。这个audio_buf的大小为 1.5倍的声音帧的大小以便于有一个比较好的缓冲,这个声音帧的大小是ffmpeg给出的。
最后解码音频
让我们看一下解码器的真正部分:audio_decode_frame
1 int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, 2 int buf_size) { 3 static AVPacket pkt; 4 static uint8_t *audio_pkt_data = NULL; 5 static int audio_pkt_size = 0; 6 int len1, data_size; 7 for(;;) { 8 while(audio_pkt_size > 0) { 9 data_size = buf_size; 10 len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf, &data_size, 11 audio_pkt_data, audio_pkt_size); 12 if(len1 < 0) { 13 audio_pkt_size = 0; 14 break; 15 } 16 audio_pkt_data += len1; 17 audio_pkt_size -= len1; 18 if(data_size <= 0) { 19 continue; 20 } 21 return data_size; 22 } 23 if(pkt.data) 24 av_free_packet(&pkt); 25 if(quit) { 26 return -1; 27 } 28 if(packet_queue_get(&audioq, &pkt, 1) < 0) { 29 return -1; 30 } 31 audio_pkt_data = pkt.data; 32 audio_pkt_size = pkt.size; 33 } 34 }
整个过程实际上从函数的尾部开始,在这里我们调用了packet_queue_get()函数。我们从队列中取出包,并且保存它的信息。然后,一旦我们有 了可以使用的包,我们就调用函数avcodec_decode_audio2(),它的功能就像它的姐妹函数 avcodec_decode_video()一样,唯一的区别是它的一个包里可能有不止一个声音帧,所以你可能要调用很多次来解码出包中所有的数据。同 时也要记住进行指针audio_buf的强制转换,因为SDL给出的是8位整型缓冲指针而ffmpeg给出的数据是16位的整型指针。你应该也会注意到 len1和data_size的不同,len1表示解码使用的数据的在包中的大小,data_size表示实际返回的原始声音数据的大小。
当我们得到一些数据的时候,我们立刻返回来看一下是否仍然需要从队列中得到更加多的数据或者我们已经完成了。如果我们仍然有更加多的数据要处理,我们把它保存到下一次。如果我们完成了一个包的处理,我们最后要释放它。
就是这样。我们利用主的读取队列循环从文件得到音频并送到队列中,然后被audio_callback函数从队列中读取并处理,最后把数据送给SDL,于是SDL就相当于我们的声卡。让我们继续并且编译:
gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm
`sdl-config --cflags --libs`
啊哈!视频虽然还是像原来那样快,但是声音可以正常播放了。这是为什么呢?因为声音信息中的采样率--虽然我们把声音数据尽可能快的填充到声卡缓冲中,但是声音设备却会按照原来指定的采样率来进行播放。
我们几乎已经准备好来开始同步音频和视频了,但是首先我们需要的是一点程序的组织。用队列的方式来组织和播放音频在一个独立的线程中工作的很好:它使得程 序更加更加易于控制和模块化。在我们开始同步音视频之前,我们需要让我们的代码更加容易处理。所以下次要讲的是:创建一个线程。