FFmpeg+SDL2實現簡易音視頻同步播放器

SDL2文章列表html

SDL2入門git

SDL2事件處理github

SDL2紋理渲染數組

SDL2音頻播放緩存

FFmpeg+SDL2實現視頻流播放ide

FFmpeg+SDL2實現音頻流播放學習

FFmpeg音視頻同步ui

通過前面一系列的SDL2學習,終於到最後實現一個完整的簡易播放器了。spa

線程模型

SimplePlayer

這是實現的簡易播放器的線程模型,經過這張圖再結合咱們以前博客中學習的內容,基本能夠了解播放器的一個總體運行流程。具體代碼也是根據這張圖來實現。.net

重要結構體

VideoState

整個播放器中最重要的結構體,解複用、解碼、音視頻同步、渲染相關參數都在該結構體中,它貫穿了整個播放流程。

typedef struct VideoState {
    
    char filename[1024]; // 文件名稱
    AVFormatContext *pFormatCtx; // 上下文
    int videoStream, audioStream; //音視頻流index


    //// 同步相關
    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;  

    //音頻相關
    AVStream *audio_st; // 音頻流
    AVCodecContext *audio_ctx; // 音頻解碼上下文
    PacketQueue audioq; // 音頻隊列
    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; // 視頻流隊列


    VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE]; // 解碼後視頻幀數組
    int pictq_size, pictq_rindex, pictq_windex;
    SDL_mutex *pictq_mutex;
    SDL_cond *pictq_cond;

    SDL_Thread *parse_tid; // 解複用線程
    SDL_Thread *video_tid;// 視頻解碼線程

    int quit; // 退出標記位
} VideoState;
複製代碼

PacketQueue

//// 解複用後音視頻packet保存隊列
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; // 音視頻同步後視頻幀應該播放的時間
} VideoPicture;
複製代碼

具體代碼

Main

  1. 初始化
  2. 建立定時器,定時視頻幀的刷新
  3. 建立解複用線程
  4. 等待事件
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);
    }
    //建立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();

    // 定時刷新器,主要用來控制視頻的刷新
    schedule_refresh(is, 40);

    // 建立解複用線程
    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: // 定時器刷新事件
                video_refresh_timer(event.user.data1);
                break;
            default:
                break;
        }
    }

    // 退出
    Destroy:
    SDL_Quit();
    return 0;

}
複製代碼

解複用

  1. 打開文件
  2. 找到音視頻流
  3. 打開音頻、視頻流,建立視頻解碼線程,準備解碼
  4. 讀取packet,將音視頻packet分別放入隊列中,等待解碼線程取出
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;
        }
    }
    // 打開音頻流,建立解碼器,配置參數
    if (audio_index >= 0) {
        stream_component_open(is, audio_index);
    }
    // 打開視頻流,建立解碼器,建立解碼線程
    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存入隊列中
        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;
}
複製代碼

視頻解碼

  1. 從隊列中取出視頻packet
  2. 解碼,同步
  3. 加碼後Frame存入數組,等待視頻渲染
//// 視頻解碼
int decode_video_thread(void *arg) {
    VideoState *is = (VideoState *) arg;
    AVPacket pkt1, *packet = &pkt1;
    AVFrame *pFrame;
    double pts;

    pFrame = av_frame_alloc();

    for (;;) {
        // 從視頻隊列中取出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;
}
複製代碼

音頻解碼

//// 音頻設備回調
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) {
                // 音頻解碼錯誤,播放靜音
                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;
    }
}
複製代碼

視頻刷新播放

//// 視頻刷新播放,並預測下一幀的播放時間,設置新的定時器
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 {
            // 從數組中取出一幀視頻幀
            vp = &is->pictq[is->pictq_rindex];

            is->video_current_pts = vp->pts;
            is->video_current_pts_time = av_gettime();
            // 當前Frame時間減去上一幀的時間,獲取兩幀間的時差
            delay = vp->pts - is->frame_last_pts;
            if (delay <= 0 || delay >= 1.0) {
                // 延時小於0或大於1秒(太長)都是錯誤的,將延時時間設置爲上一次的延時時間
                delay = is->frame_last_delay;
            }
            // 保存延時和PTS,等待下次使用
            is->frame_last_delay = delay;
            is->frame_last_pts = vp->pts;

            // 獲取音頻Audio_Clock
            ref_clock = get_audio_clock(is);
            // 獲得當前PTS和Audio_Clock的差值
            diff = vp->pts - ref_clock;

            // AV_SYNC_THRESHOLD 最小刷新時間
            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
            // diff小於非同步閾值,能夠進行同步
            if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
                if (diff <= -sync_threshold) {
                    // 視頻時間在音頻時間以前,應該讓視頻儘快播放
                    delay = 0;
                } else if (diff >= sync_threshold) {
                     // 視頻時間在音頻時間以後,應該讓視頻延遲播放
                    delay = 2 * delay;
                }
            }
            is->frame_timer += delay;
            // 最終真正要延時的時間
            actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
            if (actual_delay < 0.010) {
                // 延時時間太小就設置最小值
                actual_delay = 0.010;
            }
            // 根據延時時間從新設置定時器,刷新視頻
            schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));

            // 視頻幀顯示
            video_display(is);

            // 更新視頻幀數組下標
            if (++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
                is->pictq_rindex = 0;
            }
            SDL_LockMutex(is->pictq_mutex);
            // 視頻幀數組減一
            is->pictq_size--;
            SDL_CondSignal(is->pictq_cond);
            SDL_UnlockMutex(is->pictq_mutex);
        }
    } else {
        schedule_refresh(is, 100);
    }
}
複製代碼

大致的流程就是這樣了,相比以前的Demo複雜度會高很多,可是全部的知識在前面的博客中都有涉及,在博客中也講不了什麼東西,仍是直接本身運行,再去看代碼會更好,理清流程,整個播放器的代碼也不會很難看懂,這裏附上源碼 Github-SimplePlay

學習音視頻推薦:

第一個固然推薦雷神雷霄驊,中國FFmpeg第一人,系統地整理了FFmpeg相關的知識點,入門必看,惋惜早逝,緬懷雷神。雷霄驊的博客

第二個推薦大神李超的慕課網視頻,講得很是實用,音視頻小白入門值得一看。

相關文章
相關標籤/搜索