SDL2文章列表
經(jīng)過(guò)前面一系列的SDL2學(xué)習(xí),終于到最后實(shí)現(xiàn)一個(gè)完整的簡(jiǎn)易播放器了。
線程模型

SimplePlayer
這是實(shí)現(xiàn)的簡(jiǎn)易播放器的線程模型,通過(guò)這張圖再結(jié)合我們之前博客中學(xué)習(xí)的內(nèi)容,基本可以了解播放器的一個(gè)整體運(yùn)行流程。具體代碼也是根據(jù)這張圖來(lái)實(shí)現(xiàn)。
重要結(jié)構(gòu)體
VideoState
整個(gè)播放器中最重要的結(jié)構(gòu)體,解復(fù)用、解碼、音視頻同步、渲染相關(guān)參數(shù)都在該結(jié)構(gòu)體中,它貫穿了整個(gè)播放流程。
typedef struct VideoState {
char filename[1024]; // 文件名稱
AVFormatContext *pFormatCtx; // 上下文
int videoStream, audioStream; //音視頻流index
//// 同步相關(guān)
double audio_clock;
double frame_timer;
double frame_last_pts;
double frame_last_delay;
double video_clock;
double video_current_pts;
int64_t video_current_pts_time;
//音頻相關(guān)
AVStream *audio_st; // 音頻流
AVCodecContext *audio_ctx; // 音頻解碼上下文
PacketQueue audioq; // 音頻隊(duì)列
uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3) / 2]; // 音頻緩存
unsigned int audio_buf_size;
unsigned int audio_buf_index;
AVFrame audio_frame; // 音頻幀
AVPacket audio_pkt; // 音頻包
uint8_t *audio_pkt_data;
int audio_pkt_size;
struct SwrContext *audio_swr_ctx; // 音頻重采樣
//video
AVStream *video_st; // 視頻流
AVCodecContext *video_ctx; // 視頻流解碼上下文
PacketQueue videoq; // 視頻流隊(duì)列
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE]; // 解碼后視頻幀數(shù)組
int pictq_size, pictq_rindex, pictq_windex;
SDL_mutex *pictq_mutex;
SDL_cond *pictq_cond;
SDL_Thread *parse_tid; // 解復(fù)用線程
SDL_Thread *video_tid;// 視頻解碼線程
int quit; // 退出標(biāo)記位
} VideoState;
PacketQueue
//// 解復(fù)用后音視頻packet保存隊(duì)列
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
VideoPicture
//// 解碼后視頻幀
typedef struct VideoPicture {
AVFrame *frame;
int width, height;
double pts; // 音視頻同步后視頻幀應(yīng)該播放的時(shí)間
} VideoPicture;
具體代碼
Main
- 初始化
- 創(chuàng)建定時(shí)器,定時(shí)視頻幀的刷新
- 創(chuàng)建解復(fù)用線程
- 等待事件
int WinMain(int argc, char *argv[]) {
char *file = "C:\\Users\\lenovo\\Desktop\\IMG_5950.mp4";
SDL_Event event;
VideoState *is;
is = av_mallocz(sizeof(VideoState));
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
exit(1);
}
//創(chuàng)建SDL Window
win = SDL_CreateWindow("Media Player",
100,
100,
640, 480,
SDL_WINDOW_RESIZABLE);
if (!win) {
fprintf(stderr, "SDL_CreateWindow error,exit!", SDL_GetError());
exit(1);
}
renderer = SDL_CreateRenderer(win, -1, 0);
text_mutex = SDL_CreateMutex();
strlcpy(is->filename, file, sizeof(is->filename));
is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();
// 定時(shí)刷新器,主要用來(lái)控制視頻的刷新
schedule_refresh(is, 40);
// 創(chuàng)建解復(fù)用線程
is->parse_tid = SDL_CreateThread(demux_thread, "demux_thread", is);
if (!is->parse_tid) {
av_free(is);
return -1;
}
for (;;) {
// 等待SDL事件,否則阻塞
SDL_WaitEvent(&event);
switch (event.type) {
case FF_QUIT_EVENT:
case SDL_QUIT: // 退出
is->quit = 1;
goto Destroy;
case SDL_KEYDOWN:// ESC退出
if (event.key.keysym.sym == SDLK_ESCAPE) {
is->quit = 1;
goto Destroy;
}
break;
case FF_REFRESH_EVENT: // 定時(shí)器刷新事件
video_refresh_timer(event.user.data1);
break;
default:
break;
}
}
// 退出
Destroy:
SDL_Quit();
return 0;
}
解復(fù)用
- 打開(kāi)文件
- 找到音視頻流
- 打開(kāi)音頻、視頻流,創(chuàng)建視頻解碼線程,準(zhǔn)備解碼
- 讀取packet,將音視頻packet分別放入隊(duì)列中,等待解碼線程取出
int demux_thread(void *arg) {
if ((err_code = avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)) < 0) {
av_strerror(err_code, errors, 1024);
return -1;
}
// Find the first video stream
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
video_index < 0) {
video_index = i;
}
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
audio_index < 0) {
audio_index = i;
}
}
// 打開(kāi)音頻流,創(chuàng)建解碼器,配置參數(shù)
if (audio_index >= 0) {
stream_component_open(is, audio_index);
}
// 打開(kāi)視頻流,創(chuàng)建解碼器,創(chuàng)建解碼線程
if (video_index >= 0) {
stream_component_open(is, video_index);
// video_tid = SDL_CreateThread(decode_video_thread, "decode_video_thread", is);
}
for (;;) {
if (av_read_frame(is->pFormatCtx, packet) < 0) {
if (is->pFormatCtx->pb->error == 0) {
SDL_Delay(100); /* no error; wait for user input */
continue;
} else {
break;
}
}
// 將packet存入隊(duì)列中
if (packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if (packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_packet_unref(packet);
}
}
return 0;
}
視頻解碼
- 從隊(duì)列中取出視頻packet
- 解碼,同步
- 加碼后Frame存入數(shù)組,等待視頻渲染
//// 視頻解碼
int decode_video_thread(void *arg) {
VideoState *is = (VideoState *) arg;
AVPacket pkt1, *packet = &pkt1;
AVFrame *pFrame;
double pts;
pFrame = av_frame_alloc();
for (;;) {
// 從視頻隊(duì)列中取出packet
if (packet_queue_get(&is->videoq, packet, 1) < 0) {
break;
}
// 解碼
avcodec_send_packet(is->video_ctx, packet);
while (avcodec_receive_frame(is->video_ctx, pFrame) == 0) {
if ((pts = pFrame->best_effort_timestamp) != AV_NOPTS_VALUE) {
} else {
pts = 0;
}
pts *= av_q2d(is->video_st->time_base);
// 同步
pts = synchronize_video(is, pFrame, pts);
if (queue_picture(is, pFrame, pts) < 0) {
break;
}
av_packet_unref(packet);
}
}
av_frame_free(&pFrame);
return 0;
}
音頻解碼
//// 音頻設(shè)備回調(diào)
void audio_callback(void *userdata, Uint8 *stream, int len) {
VideoState *is = (VideoState *) userdata;
int len1, audio_size;
double pts;
SDL_memset(stream, 0, len);
while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {
// 音頻解碼
audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
if (audio_size < 0) {
// 音頻解碼錯(cuò)誤,播放靜音
is->audio_buf_size = 1024 * 2 * 2;
memset(is->audio_buf, 0, is->audio_buf_size);
} else {
is->audio_buf_size = audio_size;
}
is->audio_buf_index = 0;
}
len1 = is->audio_buf_size - is->audio_buf_index;
if (len1 > len)
len1 = len;
// 混音播放
SDL_MixAudio(stream, (uint8_t *) is->audio_buf + is->audio_buf_index, len1, SDL_MIX_MAXVOLUME);
len -= len1;
stream += len1;
is->audio_buf_index += len1;
}
}
視頻刷新播放
//// 視頻刷新播放,并預(yù)測(cè)下一幀的播放時(shí)間,設(shè)置新的定時(shí)器
void video_refresh_timer(void *userdata) {
VideoState *is = (VideoState *) userdata;
VideoPicture *vp;
double actual_delay, delay, sync_threshold, ref_clock, diff;
if (is->video_st) {
if (is->pictq_size == 0) {
schedule_refresh(is, 1);
} else {
// 從數(shù)組中取出一幀視頻幀
vp = &is->pictq[is->pictq_rindex];
is->video_current_pts = vp->pts;
is->video_current_pts_time = av_gettime();
// 當(dāng)前Frame時(shí)間減去上一幀的時(shí)間,獲取兩幀間的時(shí)差
delay = vp->pts - is->frame_last_pts;
if (delay <= 0 || delay >= 1.0) {
// 延時(shí)小于0或大于1秒(太長(zhǎng))都是錯(cuò)誤的,將延時(shí)時(shí)間設(shè)置為上一次的延時(shí)時(shí)間
delay = is->frame_last_delay;
}
// 保存延時(shí)和PTS,等待下次使用
is->frame_last_delay = delay;
is->frame_last_pts = vp->pts;
// 獲取音頻Audio_Clock
ref_clock = get_audio_clock(is);
// 得到當(dāng)前PTS和Audio_Clock的差值
diff = vp->pts - ref_clock;
// AV_SYNC_THRESHOLD 最小刷新時(shí)間
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
// diff小于非同步閾值,可以進(jìn)行同步
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold) {
// 視頻時(shí)間在音頻時(shí)間之前,應(yīng)該讓視頻盡快播放
delay = 0;
} else if (diff >= sync_threshold) {
// 視頻時(shí)間在音頻時(shí)間之后,應(yīng)該讓視頻延遲播放
delay = 2 * delay;
}
}
is->frame_timer += delay;
// 最終真正要延時(shí)的時(shí)間
actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
if (actual_delay < 0.010) {
// 延時(shí)時(shí)間過(guò)小就設(shè)置最小值
actual_delay = 0.010;
}
// 根據(jù)延時(shí)時(shí)間重新設(shè)置定時(shí)器,刷新視頻
schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));
// 視頻幀顯示
video_display(is);
// 更新視頻幀數(shù)組下標(biāo)
if (++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_rindex = 0;
}
SDL_LockMutex(is->pictq_mutex);
// 視頻幀數(shù)組減一
is->pictq_size--;
SDL_CondSignal(is->pictq_cond);
SDL_UnlockMutex(is->pictq_mutex);
}
} else {
schedule_refresh(is, 100);
}
}
大體的流程就是這樣了,相比之前的Demo復(fù)雜度會(huì)高不少,但是所有的知識(shí)在前面的博客中都有涉及,在博客中也講不了什么東西,還是直接自己運(yùn)行,再去看代碼會(huì)更好,理清流程,整個(gè)播放器的代碼也不會(huì)很難看懂,這里附上源碼 Github-SimplePlay
學(xué)習(xí)音視頻推薦:
第一個(gè)當(dāng)然推薦雷神雷霄驊,中國(guó)FFmpeg第一人,系統(tǒng)地整理了FFmpeg相關(guān)的知識(shí)點(diǎn),入門(mén)必看,可惜早逝,緬懷雷神。雷霄驊的博客
第二個(gè)推薦大神李超的慕課網(wǎng)視頻,講得非常實(shí)用,音視頻小白入門(mén)值得一看。