英文原文地址:http://dranger.com/ffmpeg/tutorial03.html
把这一切都留在脑海,因为我们实际上还没有关于音频流的任何信息!让我们回到我们的代码中找到视频流的地方,找到哪个流是音频流。
// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO
&&
videoStream < 0) {
videoStream=i;
}
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
audioStream < 0) {
audioStream=i;
}
}
if(videoStream==-1)
return -1; // Didn't find a video stream
if(audioStream==-1)
return -1;
从这里我们可以从流中获取AVCodecContext所需的所有信息,就像我们对视频流所做的一样:
AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;
aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;
如果您还记得以前的教程,我们仍然需要打开音频编解码器本身。 这很简单:
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
// Copy context
aCodecCtx = avcodec_alloc_context3(aCodec);
if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context
}
/* set up SDL Audio here */
avcodec_open2(aCodecCtx, aCodec, NULL);
包含在编解码器上下文中的是我们设置音频所需的所有信息:
wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
我们来看看这些:
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
首先,我们应该指出,nb_packets和size是不一样的 - size是指我们从packet-> size获得的字节大小。 你会注意到我们有一个互斥锁和一个condtion变量。 这是因为SDL正在将音频流程作为单独的线程运行。 如果我们不正确地锁定队列,我们可能真的搞砸了我们的数据。 我们将看到如何在队列的执行。 每个程序员都应该知道如何创建一个队列,但是我们包含了这个功能,以便学习SDL的功能。
首先我们做一个函数来初始化队列:
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 *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = 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;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
SDL_LockMutex()锁定队列中的互斥锁,所以我们可以添加一些东西,然后 SDL_CondSignal()通过我们的条件变量向我们的get函数发送一个信号(如果它在等待的话),告诉它有数据,它可以继续 ,然后解锁互斥体,让它走。
int quit = 0;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(quit) {
ret = -1;
break;
}
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;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
正如你所看到的,我们已经将这个函数封装在一个永久循环中,所以如果我们想阻塞的话,我们一定会得到一些数据。 我们通过使用SDL的SDL_CondWait()函数来避免循环。 基本上,所有的CondWait都在等待来自SDL_CondSignal()(或SDL_CondBroadcast())的信号,然后继续。 然而,看起来好像我们已经把它困在了我们的互斥量中 - 如果我们持有这个锁,我们的put函数就不能把任何东西放到队列中! 但是,SDL_CondWait()对我们来说也是解锁我们给它的互斥锁,然后一旦我们得到信号就试图再次锁定它。
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
quit = 1;
我们确保将quit标志设置为1。
PacketQueue audioq;
main() {
...
avcodec_open2(aCodecCtx, aCodec, NULL);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
SDL_PauseAudio()最终启动音频设备。 如果没有获取数据,它会播放静音; 它不会马上。
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
....
}
} else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet);
} else {
av_free_packet(&packet);
}
请注意,我们在放入队列后不会释放数据包。 我们将在解码之后释放它。
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_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) {
/* We have already sent all our data; get more */
audio_size = audio_decode_frame(aCodecCtx, audio_buf,
sizeof(audio_buf));
if(audio_size < 0) {
/* If error, output silence */
audio_buf_size = 1024;
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
len1 = audio_buf_size - audio_buf_index;
if(len1 > len)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}
}
这基本上是一个简单的循环,它将从我们将写入的另一个函数(audio_decode_frame())中提取数据,将结果存储在中间缓冲区中,尝试将len字节写入流中,并在没有足够数据的情况下获取更多数据 或者如果我们有一些遗留下来的话,将它保存下来。 audio_buf的大小是ffmpeg给我们的最大音频帧的1.5倍,这给了我们一个很好的缓冲。
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf,
int buf_size) {
static AVPacket pkt;
static uint8_t *audio_pkt_data = NULL;
static int audio_pkt_size = 0;
static AVFrame frame;
int len1, data_size = 0;
for(;;) {
while(audio_pkt_size > 0) {
int got_frame = 0;
len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
if(len1 < 0) {
/* if error, skip frame */
audio_pkt_size = 0;
break;
}
audio_pkt_data += len1;
audio_pkt_size -= len1;
data_size = 0;
if(got_frame) {
data_size = av_samples_get_buffer_size(NULL,
aCodecCtx->channels,
frame.nb_samples,
aCodecCtx->sample_fmt,
1);
assert(data_size <= buf_size);
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;
}
if(pkt.data)
av_free_packet(&pkt);
if(quit) {
return -1;
}
if(packet_queue_get(&audioq, &pkt, 1) < 0) {
return -1;
}
audio_pkt_data = pkt.data;
audio_pkt_size = pkt.size;
}
}
整个过程实际上是在函数结束的时候开始的,在这里我们调用了packet_queue_get()。我们从队列中选择数据包,并保存其信息。然后,一旦我们有一个包处理,我们称之为avcodec_decode_audio4(),其行为很像其姐妹功能,avcodec_decode_video(),除了在这种情况下,一个数据包可能有多个帧,所以你可能不得不打电话它几次从数据包中获取所有的数据。一旦我们有框架,我们简单地将它复制到我们的音频缓冲区,确保data_size比我们的音频缓冲区小。此外,请记住强制转换为audio_buf,因为SDL提供了一个8位整型缓冲区,ffmpeg在16位整型缓冲区中提供了数据。你也应该注意到len1和data_size的区别。 len1是我们使用的数据包数量,data_size是返回的原始数据量。
gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`
万岁! 视频仍在尽可能快地播放,但音频正在播放。 为什么是这样? 这是因为音频信息具有采样率 - 我们正在尽可能快地抽出音频信息,但是音频只是根据采样率从该流中随意播放。