ffmpeg音视频编码入门:视频解码
ffmpeg音视频编码入门:音频解码(acc/mp3 转 pcm)
ffmpeg —— SDL2播放yuv文件(使用事件驱动和多线程,支持按键暂停/退出)
ffmpeg —— SDL2播放pcm音频
ffmpeg 视频文件解封装,提取mp4中的h264码流和aac码流
项目思路:
MediaState描述了媒体文件的信息,是最顶层的结构;
struct MediaState {
AVFormatContext *fmt_ctx; // 输入流的格式上下文
struct VideoState *video_state; // 视频结构
struct AudioState *audio_state; // 音频结构
SDL_Thread *demuxer_tid; // 解封装线程
SDL_Thread *playback_control_tid; // 播放控制线程
SDL_Event event; // 按键事件,播放/暂停/退出
};
StreamState描述了解码器和码流管理,是最底层的结构;
struct StreamState {
int stream_index; // 码流下标
AVCodecContext *cod_ctx; // 解码器上下文
AVCodec *cod; // 解码器
float fps; // 视频播放延时为1/fps
struct myqueue *que; // 可播放的码流队列
// 优化队列的管理:解封装完成,队列空时不需要等待数据
int writer_count; // 写者数量,对应解封装线程
int reader_count; // 读者数量,对应取数据解码线程
};
VideoState和AudioState描述了音频和视频码流解码播放的相关信息。
struct AudioState {
// 解码相关
struct StreamState *stream_state;
int pcmbuf_size;
uint8_t **pcmbuffer;
struct SwrContext *swr_ctx;
// 播放相关
SDL_AudioSpec spec;
unsigned int audio_len;
unsigned char *audio_chunk;
unsigned char *audio_pos;
SDL_Thread *audio_tid;
};
struct VideoState {
struct StreamState *stream_state;
AVBSFContext *bsf_ctx;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
SDL_Rect rect;
SDL_Thread *video_tid;
};
int demuxer_thread(void *p) {
struct MediaState *media = (struct MediaState *)p;
struct VideoState *video = media->video_state;
struct AudioState *audio = media->audio_state;
// 读取一帧编码数据
while (1) {
HANDLE_EVENT(media->event.type);
AVPacket *packet = av_packet_alloc();
if (av_read_frame(media->fmt_ctx, packet) != 0) break;
/*************** 处理码流 *******************/
if (packet->stream_index == video->stream_state->stream_index) {
// 过滤器处理视频码流
if (av_bsf_send_packet(video->bsf_ctx, packet) != 0) {
printf("failed to send packet to bitstream filter\n");
break;
}
if (av_bsf_receive_packet(video->bsf_ctx, packet) != 0) {
printf("failed to receive packet from bitstream filter\n");
break;
}
// 将码流数据放到队列中管理
if (enqueue(video->stream_state->que, packet, video->stream_state->reader_count) < 0) {
break; // 解码线程退出时才会出现返回-1的情况
}
} else if (packet->stream_index == audio->stream_state->stream_index) {
// 添加ADTS头
AVPacket *tmp = av_packet_alloc();
tmp->size = packet->size + 7;;
tmp->data = malloc(tmp->size);
memcpy(tmp->data, adts_header_gen(tmp->size), 7);
memcpy(tmp->data + 7, packet->data, packet->size);
av_packet_free(&packet);
if (enqueue(audio->stream_state->que, tmp, audio->stream_state->reader_count) < 0) {
break;
}
} else {
printf("unkown stream\n");
av_packet_free(&packet);
break;
}
// 视频24fps,40ms播放一帧,音频44100/1024 = 42fps,22ms播放一帧
// 当大于缓冲区一半时,延时使消耗大于产出,que->size减小
if (video->stream_state->que->size > MAX_QUEUE/2)
SDL_Delay(5*video->stream_state->que->size);
}
audio->stream_state->writer_count = 0;
video->stream_state->writer_count = 0;
printf("demuxer_thread end\n");
}
int playback_control_thread(void *p) {
struct MediaState *media = (struct MediaState *)p;
media->audio_state->audio_tid = SDL_CreateThread(audio_thread, NULL, media);
media->video_state->video_tid = SDL_CreateThread(video_thread, NULL, media);
SDL_Thread *refresh_tid = SDL_CreateThread(refresh_thread, NULL, NULL);
while (1) {
if (media->event.type == SDL_KEYDOWN) {
// 空格键暂停,ESC键退出
if (media->event.key.keysym.sym == SDLK_SPACE) {
thread_stop = !thread_stop;
} else if (media->event.key.keysym.sym == SDLK_ESCAPE) {
thread_quit = 1;
}
} else if (media->event.type == SDL_QUIT || media->event.type == QUIT_EVENT) {
break;
}
// 等待刷新,刷新间隔越小,实时性越好
SDL_WaitEvent(&media->event);
}
SDL_WaitThread(refresh_tid, NULL);
SDL_WaitThread(media->audio_state->audio_tid, NULL);
SDL_WaitThread(media->video_state->video_tid, NULL);
printf("playback_control_thread end\n");
}
int audio_thread(void *p) {
struct MediaState *media = (struct MediaState *)p;
struct AudioState *audio = media->audio_state;
AVFrame *frame = av_frame_alloc();
// 启动播放
SDL_PauseAudio(0);
while (1) {
HANDLE_EVENT(media->event.type);
// 从码流队列中取数据, 解缓冲区无数据且封装已完成时退出
AVPacket *packet = dequeue(audio->stream_state->que, audio->stream_state->writer_count);
if (packet == NULL) break;
int ret = decodec_packet_to_frame(audio->stream_state->cod_ctx, packet, frame);
if (ret == EAGAIN) continue;
else if (ret == EINVAL) break;
SDL_play_pcm(audio, frame);
av_packet_free(&packet); // 播放完一帧后清理
av_frame_unref(frame);
}
av_frame_free(&frame);
audio->stream_state->reader_count--;
//queue_wakeup(audio->stream_state->que);
printf("audio_thread end\n");
}
问题:暂停操作正常,但解封装线程无法中途退出
,导致主线程一直卡在SDL_WaitThread(media->demuxer_tid, NULL)。
猜测原因是队列操作的影响(使用了条件变量,而取数据解码的速度总是慢于解封装码流入队的速度,所以经常队列满,卡在pthread_wait_cond)。
虽然添加了读者写者的条件,但因为没有数据变化的广播,所以解封装线程被阻塞在入队函数中的pthread_cond_wait位置
。
int enqueue(struct myqueue *q, Elem_t t, int reader_count) {
if (reader_count <= 0) return -1; // 读者不存在,存数据到队列没有意义
pthread_mutex_lock(&q->mut);
while (q->size == MAX_QUEUE && reader_count > 0) {
pthread_cond_wait(&q->cond, &q->mut);
}
q->buffer[q->tail] = t;
q->tail = next_pos(q->tail);
q->size++;
pthread_cond_broadcast(&q->cond);
pthread_mutex_unlock(&q->mut);
return 0;
}
①可以考虑添加一个周期检测读者/写者是否都存在的线程,如果有一方缺失,则调用pthread_cond_broadcast,唤醒被阻塞的线程。(加一个管理线程/每个队列都拥有一个检测线程?未尝试)
②在解码线程退出时,更新读者数量,并用广播唤醒被阻塞的线程,退出时手动调用pthread_cond_broadcast并没有唤醒解封装线程
。
③经常卡在pthread_wait_cond,这是读写速度差异太大导致
的。可以适当减慢一下解码的速度,从而避免队列满的情况。但只是固定延时
的话,无法杜绝被阻塞的情况。(只要速度不均衡,最终还是会走向队列满,越往后越拥塞
)。
④根据缓冲区大小,及音视频播放帧率,动态调整延时。缓冲区中的码流数据越多,延时越久,从而使que->size保持在一个稳定范围,以避免线程被阻塞。最好是初期不延时,当数据过半时,使其快速稳定。(自动控制原理?2333)
问题2:只是用多线程,并发的解码播放音频和视频,没有提供自适应的同步机制。
问题3:可以尝试抽象统一音频和视频的码流方法。其它:。。。(解耦,封装与抽象)