FFmpeg編程(四)SDL與FFmpeg的聯合使用


一:簡單的播放器V1(只播放視頻)

(一)回顧

FFmpeg編程(二)FFmpeg中級開發

FFmpeg編程(三)SDL開發

(二)FFmpeg與SDL的簡單結合

#include <stdio.h>
#include <SDL.h>

#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>

int main(int argc,char* argv[]){
    char* in_file = NULL;

    int ret = -1;            //返回值
    int len;                //存放讀取的數據大小
    int stream_idx;            //存放視頻流的索引
    int frameFin;            //標志packet中數據幀是否讀取完成

    AVFormatContext* fmt_cxt = NULL;    //格式上下文
    AVCodecContext* codec_cxt = NULL;    //編解碼器上下文
    struct SwsContext* sws_cxt = NULL;    //圖像處理上下文

    AVCodec* codec = NULL;    //編解碼器
    AVFrame* frame = NULL;    //
    AVPicture* pict = NULL;    //YUV圖像存放

    AVPacket packet;        //

    SDL_Window* win = NULL;    //SDL窗口指針
    SDL_Renderer* rend = NULL;    //SDL渲染器指針
    SDL_Texture* text = NULL;    //紋理

    SDL_Rect rect;    //用於顯示數據的矩陣區域

    SDL_Event event;    //SDL事件

    int w_width = 640;    //設置的默認窗口大小,后面會進行調整
    int w_height = 480;

    if(argc<2){        //檢查是否設置了播放的文件名稱
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Usage: command <file>");
        return ret;
    }
    in_file = argv[1];

    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to initialize SDL - %s!",SDL_GetError());
        return ret;
    }
    //------------------------配置所有的FFmpeg信息
    
    av_register_all();    //注冊所有協議

    //獲取格式上下文
    if(avformat_open_input(&fmt_cxt,in_file,NULL,NULL)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to open video!");
        goto __INIT;
    }

    //獲取流的信息,這里換一種方式獲取,不直接使用av_find_best_stream獲取
    stream_idx = -1;
    for(int i=0;i<fmt_cxt->nb_streams;i++){
        if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
            stream_idx = i;
            break;
        }
    }

    if(stream_idx==-1){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream!");
        goto __FORMAT;
    }

    //打印輸入視頻流的詳細信息
    if(avformat_find_stream_info(fmt_cxt,0)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream information!");
        goto __FORMAT;
    }

    av_dump_format(fmt_cxt,stream_idx,in_file,0);    //打印信息

    //開始獲取解碼器,對視頻流進行解碼獲取YUV數據
    //根據輸入流的參數獲取對於的解碼器
    codec = avcodec_find_decoder(fmt_cxt->streams[stream_idx]->codec->codec_id);
    if(codec==NULL){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder!");
        goto __FORMAT;
    }

    //獲取對應上下文
    codec_cxt = avcodec_alloc_context3(codec);
    if(!codec_cxt){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder context!");
        goto __FORMAT;
    }

    //開始拷貝輸入流參數到解碼器中
    if(avcodec_copy_context(codec_cxt,fmt_cxt->streams[stream_idx]->codec)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy video decoder context!");
        goto __CODEC;
    }

    //打開編解碼器
    if(avcodec_open2(codec_cxt,codec,NULL)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn video decoder!");
        goto __CODEC;
    }

    frame = av_frame_alloc();    //分配幀
    if(!frame){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video frame!");
        goto __CODEC;
    }

    //創建圖片轉換上下文(在上面參數拷貝后面)-----------------
    //源圖像寬、高、像素格式;目標圖像寬、高、像素格式;以及圖像拉伸使用的算法
    sws_cxt = sws_getContext(codec_cxt->width,codec_cxt->height,codec_cxt->pix_fmt,
        codec_cxt->width,codec_cxt->height,AV_PIX_FMT_YUV420P,    //輸出像素格式YUV
        SWS_BILINEAR,NULL,NULL,NULL);    //獲取圖像處理上下文

    pict = (AVPicture*)malloc(sizeof(AVPicture));
    if(!pict){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video picture frame!");
        goto __FRAME;
    }
    avpicture_alloc(pict,
        AV_PIX_FMT_YUV420P,
        codec_cxt->width,
        codec_cxt->height);    //為圖像存放分配空間

    //------------------------開始設置SDL數據
    w_width = codec_cxt->width;
    w_height = codec_cxt->height;

    //創建窗口
    win = SDL_CreateWindow("Media Player",
        SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,    //不指定左上位置
        w_width,w_height,            //設置窗口寬高,與視頻一般或者大些都可以
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);    //窗口可用於OpenGL上下文 窗口可以調整大小
    if(!win){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create window to SDL");
        goto __PICT;
    }

    //創建渲染器
    rend = SDL_CreateRenderer(win,-1,0);
    if(!rend){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create renderer to SDL");
        goto __WIN;
    }

    //創建紋理
    text = SDL_CreateTexture(rend,SDL_PIXELFORMAT_IYUV,
        SDL_TEXTUREACCESS_STREAMING,    //視頻流,連續的
        w_width,w_height);
    if(!text){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create texture to SDL");
        goto __RENDER;
    }
    
    rect.x = 0;    //顯示矩陣的左上點
    rect.y = 0;
    //------------------------開始讀取數據,結合SDL與FFmpeg
    av_init_packet(&packet);    //初始化數據包
    while(av_read_frame(fmt_cxt,&packet)>=0){        
        if(packet.stream_index==stream_idx){    //屬於視頻流
            len = avcodec_decode_video2(codec_cxt,frame,&frameFin,&packet);    //作用是解碼一幀視頻數據。輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼后的結構體AVFrame。
            if(len<0){
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Fail to decode video frame");
                goto __INIT;
            }

            if(frameFin){    //表示讀取完成了一幀,開始進行轉換,解碼數據已經放入了frame中
                sws_scale(sws_cxt,(uint8_t const * const *)frame->data,frame->linesize,    //當前處理區域的每個通道數據指針,每個通道行字節數
                    0,codec_cxt->height,    //參數int srcSliceY, int srcSliceH,定義在輸入圖像上處理區域,srcSliceY是起始位置,srcSliceH是處理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性處理完整個圖像。
                    pict->data,pict->linesize);     //定義輸出圖像信息(輸出的每個通道數據指針,每個通道行字節數)

                //可以渲染YUV數據
                SDL_UpdateYUVTexture(text,NULL,
                    pict->data[0],pict->linesize[0],
                    pict->data[1],pict->linesize[1],
                    pict->data[2],pict->linesize[2]);

                //更新矩陣的信息
                rect.w = codec_cxt->width;
                rect.h = codec_cxt->height;

                SDL_RenderClear(rend);                   //清空渲染器緩沖區

                SDL_RenderCopy(rend,text,NULL,&rect);    //拷貝紋理到顯卡,選擇渲染目標的一塊矩形區域作為輸出
                SDL_RenderPresent(rend);                //將結果顯示到窗口
            }
        }

        av_packet_unref(&packet);    //注意:減少引用不會釋放空間,因為本函數還在引用這個packet結構體;
        //所以我們在解碼函數中剩余的其他幀數據,還是保留在packet中的,后面根據av_read_frame繼續向內部添加數據

        //設置事件輪詢
        SDL_PollEvent(&event);
        switch(event.type){
            case SDL_QUIT:
                goto __QUIT;
                break;
            default:
                break;
        }

    }

__QUIT:
    ret = 0;
    av_packet_unref(&packet);    //減少引用,使得自己釋放空間
    SDL_DestroyTexture(text);
__RENDER:
    SDL_DestroyRenderer(rend);
__WIN:
    SDL_DestroyWindow(win);
__PICT:
    avpicture_free(pict);
    free(pict);
__FRAME:
    av_frame_free(&frame);
__CODEC:
    avcodec_close(codec_cxt);
__FORMAT:
    avformat_close_input(&fmt_cxt);
__INIT:
    SDL_Quit();

    return ret;
}
View Code
gcc playerV1.c -o play_1 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil
./play_1 gfxm.mp4

二:簡單的播放器V2(播放視頻和音頻,未同步)

(一)回顧鎖與條件變量

SDL中的互斥量和條件變量

(二)基於隊列實現音頻數據的音視頻播放器

#include <stdio.h>
#include <assert.h>
#include <SDL.h>

#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>

#define SDL_AUDIO_BUFFER_SIZE 1024    //對於AAC,一幀數據中單通道獲取 1024個 sample采樣點
#define    MAX_AUDIO_FRAME_SIZE 192000        //是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s

//定義隊列
typedef struct PacketQueue{
    AVPacketList* first_pkt,* last_pkt;
    int nb_packets;        //數據包個數
    int size;            //隊列全部數據量大小
    SDL_mutex* mutex;    //隊列中的鎖
    SDL_cond* cond;        //條件變量
}PacketQueue;

struct SwrContext* adswr_cxt = NULL;    //全局音頻重采樣上下文
PacketQueue audioq;        //定義全局音頻數據包隊列
int quit = 0;            //標志音頻數據是否全部讀取完成

//隊列初始化數據成員
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* pktl;

    //先對數據包加引用,使得不被提前釋放
    /*
    原本數據采訪在AVPacket成員---->AVBufferRef *buf中,由av_malloc申請的獨立的buffer(unshared buffer);
    使用av_dup_packet后,拷貝了一份數據到data成員中,重新設置pkt->data終於有自己的獨立內存了,不用共享別的AVPacket的內存

    https://blog.csdn.net/weixin_34416649/article/details/92075939
    https://blog.csdn.net/wangshilin/article/details/8186608
    當某個AVPacket結構的數據緩沖區不再被使用時,要需要通過調用 av_free_packet 釋放。av_free_packet調用的是結構體本身的destruct函數

    安全起見,如果用戶希望 自由地使用一個FFMPEG內部創建的AVPacket結構,最好調用av_dup_packet進行緩沖區的克隆,
    將其轉化為緩沖區能夠被釋放的 AVPacket,以免對緩沖區的不當占用造成異常錯誤。
    av_dup_packet會為destruct指針為 av_destruct_packet_nofree的AVPacket新建一個緩沖區,然后將原緩沖區的數據拷貝至新緩沖區,置data的值為新緩沖區 的地址,
    同時設destruct指針為av_destruct_packet。
    */
    if(av_dup_packet(pkt)){    //使得更加安全,保證了數據不被別人共享,但是不能保證不被av_packet_unref刪除,所以我們在主函數中處理音頻數據包的時候,不要調用unref刪除
        return -1;    //原本數據在buffer中,但是put操作將內容放入了data中,使得packet使用=賦值可以獲取到我們想要的數據起始地址!!!!
    }

    pktl = av_malloc(sizeof(AVPacketList));
    if(!pktl){
        return -1;
    }

    pktl->pkt = *pkt;
    pktl->next = NULL;

    SDL_LockMutex(q->mutex);    //加鎖,開始添加
    if(!q->last_pkt){
        q->first_pkt = pktl;
    }else{
        q->last_pkt->next = pktl;
    }

    q->last_pkt = pktl;
    q->nb_packets++;
    q->size += pktl->pkt.size;

    SDL_CondSignal(q->cond);    //重點:對於條件變量,發送信號前會進行解鎖,發送后進行加鎖------------
    SDL_UnlockMutex(q->mutex);
    return 0;
}

int packet_queue_get(PacketQueue* q,AVPacket* pkt,int block){
    AVPacketList* pktl;
    int ret;

    SDL_LockMutex(q->mutex);    //讀取前加鎖
    for(;;){    //如果沒有數據則進行等待(當然不是一直加鎖狀態下死等,后面會使用條件變量,使得先解鎖,等待信號量到達,再加鎖處理)
        if(quit){    //音頻數據全部讀取完成,退出
            ret = -1;
            break;
        }

        pktl = q->first_pkt;
        if(pktl){    //隊列中存在數據,直接讀取
            q->first_pkt = pktl->next;
            if(!q->first_pkt)    //原本只有一個數據時候,尾指針也指向這個數據,所以也要處理,置為空
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pktl->pkt.size;
            *pkt = pktl->pkt;
            av_free(pktl);    //只是釋放了AVPakcetList,並沒有釋放內部的packet數據
            ret = 1;
            break;    //數據已經讀取到pkt中了
        }else if(!block){    //隊列中不存在數據,並且設置了block,不進行阻塞等待條件變量
            ret = 0;    //這時並沒有返回如何數據,我們對應處理設置靜默音即可
            break;
        }else{    //需要阻塞等待獲取到數據,需要設置條件變量
            SDL_CondWait(q->cond,q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

//對數據包進行解碼返回
int audio_decode_frame(AVCodecContext* adcodec_cxt,uint8_t* buf,int size){
    //下面是靜態存儲區全局變量
    static AVPacket pkt;
    static uint8_t* audio_pkt_data = NULL;
    static int audio_pkt_size = 0;    //用來存放從隊列中獲取的packet大小
    static AVFrame frame;

    int len1,data_size=0;

    for(;;){
        while(audio_pkt_size>0){    //pakcet中還有數據沒有處理完成
            int got_frame = 0;
            len1 = avcodec_decode_audio4(adcodec_cxt,&frame,&got_frame,&pkt);    //從pkt中解碼packet數據為一幀一幀的frame數據,然后由swr進行重采樣處理
            if(len1<0){    //解碼出錯,跳過該packet的數據
                audio_pkt_size = 0;
                break;
            }

            audio_pkt_data += len1;
            audio_pkt_size -= len1;
            data_size = 0;
            if(got_frame){    //獲取了一幀數據
                //dst_size = av_samples_get_buffer_size(NULL,out_channels,out_nb_samples,sample_fmt,1);    //這是每次采樣的數據 = 通道數×每個通道采樣(每幀)×格式
                data_size = 2*2*frame.nb_samples;    //雙通道,16位深

                assert(data_size<=data_size);
                swr_convert(adswr_cxt,&buf,
                    MAX_AUDIO_FRAME_SIZE*3/2,
                    (const uint8_t**)frame.data,
                    frame.nb_samples);    //將重采樣結果放入buf中去
            }
            if(data_size<=0){    //==0,表示我們的packet中的剩余數據還沒有達到獲取一幀的數據大小,繼續去獲取    
                continue;
            }
            return data_size;    //已經獲取了一幀數據,返回
        }

        //不在上面while循環,說明需要去獲取隊列中數據
        if(pkt.data)    //先判斷pkt是否清空
            av_free_packet(&pkt);    

        if(quit)
            return -1;

        if(packet_queue_get(&audioq,&pkt,1)<0){
            return -1;
        }

        audio_pkt_data = pkt.data;    //賦予初始值--------重點:原本數據在buffer中,但是前面的put操作將內容放入了data中
        audio_pkt_size = pkt.size;
    }

}

//設置實現音頻聲卡回調函數,用於聲卡主動獲取數據
void audio_callback(void* userdata,uint8_t* stream,int len){    
    //先獲取編解碼上下文
    AVCodecContext* adcodec_cxt = (AVCodecContext*)userdata;
    int audio_size,len1;    //每次讀取的數據包的大小,和,每次可以讀取的數據大小
    //設置全局靜態存儲區變量
    static uint8_t audio_buf[MAX_AUDIO_FRAME_SIZE*3/2];
    static unsigned int audio_buf_size = 0;        //音頻數據大小
    static unsigned int audio_buf_index = 0;    //音頻數據起始位置,當當前數據處理完成,即index到達最后size,就開始解碼下一個音頻數據包

    while(len>0){    //聲卡需要的數據長度
        if(audio_buf_index>=audio_buf_size){    //開始去獲取下一個數據包
            audio_size = audio_decode_frame(adcodec_cxt,audio_buf,sizeof(audio_buf));
            if(audio_size<0){    //設置靜默聲音
                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;
        }
        printf("index=%d,len1=%d,len=%d\n",audio_buf_index,len1,len);
        memcpy(stream,(uint8_t*)audio_buf+audio_buf_index,len1);
        len -= len1;    //開始改變數據位置
        stream += len1;
        audio_buf_index += len1;
    }
}


int main(int argc,char* argv[]){
    char* in_file = NULL;

    int ret = -1;            //返回值
    int len;                //存放讀取的數據大小
    int vdstream_idx;            //存放視頻流的索引
    int adstream_idx;        //存放視頻流的索引
    int frameFin;            //標志packet中數據幀是否讀取完成

    AVFormatContext* fmt_cxt = NULL;    //格式上下文
    struct SwsContext* sws_cxt = NULL;    //圖像處理上下文

    //----------視頻編解碼器----------
    AVCodec* vdcodec = NULL;    //編解碼器
    AVCodecContext* vdcodec_cxt = NULL;    //編解碼器上下文

    //----------音頻編解碼器----------
    AVCodec* adcodec = NULL;
    AVCodecContext* adcodec_cxt = NULL;    

    AVFrame* frame = NULL;    //
    AVPicture* pict = NULL;    //YUV圖像存放

    AVPacket packet;        //

    SDL_Window* win = NULL;    //SDL窗口指針
    SDL_Renderer* rend = NULL;    //SDL渲染器指針
    SDL_Texture* text = NULL;    //紋理

    SDL_Rect rect;    //用於顯示數據的矩陣區域

    int w_width = 640;    //設置的默認窗口大小,后面會進行調整
    int w_height = 480;

    SDL_Event event;    //SDL事件

    SDL_AudioSpec new_spec,old_spec;    //用來設置音頻參數,分別表示原始數據和設置的新參數
    int64_t in_channel_layout;    //音頻的布局
    int64_t out_channel_layout;

    if(argc<2){        //檢查是否設置了播放的文件名稱
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Usage: command <file>");
        return ret;
    }
    in_file = argv[1];

    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to initialize SDL - %s!",SDL_GetError());
        return ret;
    }
    //------------------------配置所有的FFmpeg信息
    
    av_register_all();    //注冊所有協議

    //獲取格式上下文
    if(avformat_open_input(&fmt_cxt,in_file,NULL,NULL)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to open video!");
        goto __FAIL;
    }

    //獲取流的信息,這里換一種方式獲取,不直接使用av_find_best_stream獲取
    vdstream_idx = -1;
    adstream_idx = -1;
    for(int i=0;i<fmt_cxt->nb_streams;i++){
        if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && vdstream_idx<0){
            vdstream_idx = i;
        }
        if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && adstream_idx<0){
            adstream_idx = i;
        }
    }

    if(vdstream_idx==-1 || adstream_idx==-1){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream or audio stream!");
        goto __FAIL;
    }

    //打印輸入視頻流的詳細信息
    if(avformat_find_stream_info(fmt_cxt,0)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream information!");
        goto __FAIL;
    }

    av_dump_format(fmt_cxt,vdstream_idx,in_file,0);    //打印信息
    av_dump_format(fmt_cxt,adstream_idx,in_file,0);    //打印信息

    //-----------------處理音頻編解碼器---------------
    adcodec = avcodec_find_decoder(fmt_cxt->streams[adstream_idx]->codec->codec_id);
    if(adcodec==NULL){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find audio decoder!");
        goto __FAIL;
    }

    adcodec_cxt = avcodec_alloc_context3(adcodec);
    if(!adcodec_cxt){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find adio decoder context!");
        goto __FAIL;
    }

    if(avcodec_copy_context(adcodec_cxt,fmt_cxt->streams[adstream_idx]->codec)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy audio decoder context!");
        goto __FAIL;
    }

    //打開編解碼器
    if(avcodec_open2(adcodec_cxt,adcodec,NULL)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn audio decoder!");
        goto __FAIL;
    }

    //-----------------處理音頻數據---------------
    new_spec.freq = adcodec_cxt->sample_rate;
    new_spec.format = AUDIO_S16SYS;
    new_spec.channels = adcodec_cxt->channels;
    new_spec.silence = 0;
    new_spec.samples = SDL_AUDIO_BUFFER_SIZE;    
    new_spec.callback = audio_callback;
    new_spec.userdata = adcodec_cxt;

    if(SDL_OpenAudio(&new_spec,&old_spec)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn audio device - %s!",SDL_GetError());
        goto __FAIL;
    }

    packet_queue_init(&audioq);    //初始化音頻數據包隊列

    in_channel_layout = av_get_default_channel_layout(adcodec_cxt->channels);    //獲取輸入流的通道布局
    out_channel_layout = in_channel_layout;    //輸出流通道布局不變
    printf("in layout:%ld, out layout:%ld\n",in_channel_layout,out_channel_layout);

    adswr_cxt = swr_alloc();    //分配音頻重采樣上下文
    if(!adswr_cxt){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc audio resample context!");
        goto __FAIL;
    }

    //----------創建重采樣的上下文
    swr_alloc_set_opts(adswr_cxt,             //設置已經創建好的上下文,如果沒有,則為NULL
                    out_channel_layout,       //設置輸出目標的通道布局(雙聲道,立體聲,...,方位增寬)
                    AV_SAMPLE_FMT_S16,        //設置輸出目標的采樣格式,與上面的AUDIO_S16SYS保持一致
                    adcodec_cxt->sample_rate, //設置輸出目標的采樣率
                    in_channel_layout,        //輸入數據的通道布局,是雙聲道
                    adcodec_cxt->sample_fmt,  //輸入數據的采樣格式為s16le
                    adcodec_cxt->sample_rate, //輸入的采樣率
                    0,                        //日志級別
                    NULL);                    //日志上下文

    swr_init(adswr_cxt);

    SDL_PauseAudio(0);    //開始播放音頻

    //-----------------處理視頻編解碼器---------------
    //開始獲取解碼器,對視頻流進行解碼獲取YUV數據
    //根據輸入流的參數獲取對於的解碼器
    vdcodec = avcodec_find_decoder(fmt_cxt->streams[vdstream_idx]->codec->codec_id);
    if(vdcodec==NULL){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder!");
        goto __FAIL;
    }

    //獲取對應上下文
    vdcodec_cxt = avcodec_alloc_context3(vdcodec);
    if(!vdcodec_cxt){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder context!");
        goto __FAIL;
    }

    //開始拷貝輸入流參數到解碼器中
    if(avcodec_copy_context(vdcodec_cxt,fmt_cxt->streams[vdstream_idx]->codec)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy video decoder context!");
        goto __FAIL;
    }

    //打開編解碼器
    if(avcodec_open2(vdcodec_cxt,vdcodec,NULL)<0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn video decoder!");
        goto __FAIL;
    }

    //-----------------處理視頻數據---------------

    frame = av_frame_alloc();    //分配幀
    if(!frame){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video frame!");
        goto __FAIL;
    }

    //創建圖片轉換上下文(在上面參數拷貝后面)-----------------
    //源圖像寬、高、像素格式;目標圖像寬、高、像素格式;以及圖像拉伸使用的算法
    sws_cxt = sws_getContext(vdcodec_cxt->width,vdcodec_cxt->height,vdcodec_cxt->pix_fmt,
        vdcodec_cxt->width,vdcodec_cxt->height,AV_PIX_FMT_YUV420P,    //輸出像素格式YUV
        SWS_BILINEAR,NULL,NULL,NULL);    //獲取圖像處理上下文

    pict = (AVPicture*)malloc(sizeof(AVPicture));
    if(!pict){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video picture frame!");
        goto __FAIL;
    }
    avpicture_alloc(pict,
        AV_PIX_FMT_YUV420P,
        vdcodec_cxt->width,
        vdcodec_cxt->height);    //為圖像存放分配空間

    //------------------------開始設置SDL數據
    w_width = vdcodec_cxt->width;
    w_height = vdcodec_cxt->height;

    //創建窗口
    win = SDL_CreateWindow("Media Player",
        SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,    //不指定左上位置
        w_width,w_height,            //設置窗口寬高,與視頻一般或者大些都可以
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);    //窗口可用於OpenGL上下文 窗口可以調整大小
    if(!win){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create window to SDL");
        goto __FAIL;
    }

    //創建渲染器
    rend = SDL_CreateRenderer(win,-1,0);
    if(!rend){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create renderer to SDL");
        goto __FAIL;
    }

    //創建紋理
    text = SDL_CreateTexture(rend,SDL_PIXELFORMAT_IYUV,
        SDL_TEXTUREACCESS_STREAMING,    //視頻流,連續的
        w_width,w_height);
    if(!text){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create texture to SDL");
        goto __FAIL;
    }
    
    rect.x = 0;    //顯示矩陣的左上點
    rect.y = 0;
    //------------------------開始讀取數據,結合SDL與FFmpeg
    av_init_packet(&packet);    //初始化數據包
    while(av_read_frame(fmt_cxt,&packet)>=0){        
        if(packet.stream_index == vdstream_idx){    //屬於視頻流
            len = avcodec_decode_video2(vdcodec_cxt,frame,&frameFin,&packet);    //作用是解碼一幀視頻數據。輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼后的結構體AVFrame。
            if(len<0){
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Fail to decode video frame");
                goto __FAIL;
            }

            if(frameFin){    //表示讀取完成了一幀,開始進行轉換,解碼數據已經放入了frame中
                sws_scale(sws_cxt,(uint8_t const * const *)frame->data,frame->linesize,    //當前處理區域的每個通道數據指針,每個通道行字節數
                    0,vdcodec_cxt->height,    //參數int srcSliceY, int srcSliceH,定義在輸入圖像上處理區域,srcSliceY是起始位置,srcSliceH是處理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性處理完整個圖像。
                    pict->data,pict->linesize);     //定義輸出圖像信息(輸出的每個通道數據指針,每個通道行字節數)

                //可以渲染YUV數據
                SDL_UpdateYUVTexture(text,NULL,
                    pict->data[0],pict->linesize[0],
                    pict->data[1],pict->linesize[1],
                    pict->data[2],pict->linesize[2]);

                //更新矩陣的信息
                rect.w = vdcodec_cxt->width;
                rect.h = vdcodec_cxt->height;

                SDL_RenderClear(rend);                   //清空渲染器緩沖區

                SDL_RenderCopy(rend,text,NULL,&rect);    //拷貝紋理到顯卡,選擇渲染目標的一塊矩形區域作為輸出
                SDL_RenderPresent(rend);                //將結果顯示到窗口
            }
            av_packet_unref(&packet);    //注意:減少引用不會釋放空間,因為本函數還在引用這個packet結構體;
            //所以我們在解碼函數中剩余的其他幀數據,還是保留在packet中的,后面根據av_read_frame繼續向內部添加數據
        }else if(packet.stream_index == adstream_idx){    //處理音頻流
            packet_queue_put(&audioq,&packet);    //重點:對於音頻數據包我們是暫存放在隊列中,所以不能馬上刪除!!!!
        }else{
            av_packet_unref(&packet);    //注意:減少引用不會釋放空間,因為本函數還在引用這個packet結構體;
            //所以我們在解碼函數中剩余的其他幀數據,還是保留在packet中的,后面根據av_read_frame繼續向內部添加數據
        }


        //設置事件輪詢
        SDL_PollEvent(&event);
        switch(event.type){
            case SDL_QUIT:
                goto __QUIT;
                break;
            default:
                break;
        }

    }

__QUIT:
    ret = 0;
__FAIL:
    av_packet_unref(&packet);    //減少引用,使得自己釋放空間
    
    if(frame){
        av_frame_free(&frame);
    }

    if(text){
        SDL_DestroyTexture(text);
    }

    if(rend){
        SDL_DestroyRenderer(rend);
    }

    if(win){
        SDL_DestroyWindow(win);
    }

    if(pict){
        avpicture_free(pict);
        free(pict);
    }

    if(adcodec_cxt){
        avcodec_close(adcodec_cxt);
    }

    if(vdcodec_cxt){
        avcodec_close(vdcodec_cxt);
    }

    if(fmt_cxt){
        avformat_close_input(&fmt_cxt);
    }
    
    SDL_Quit();
    return ret;
}
View Code
gcc playerV2.c -o play_2 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil -lswresample

三:簡單的播放器V3(線程實現播放視頻和音頻,未同步)

(一)線程模型

1.主線程:事件處理、視頻渲染等簡單事件,不做過多事情,使得響應更快。將其它復雜的邏輯放入子線程當中

2.解復用線程:對文件數據進行解復用,將視頻流放入視頻流隊列,音頻流放入音頻流隊列當中,然后創建子線程進行處理(解碼處理占CPU)

3.視頻解碼線程:解碼視頻流隊列中的數據,將結果放入解碼后的視頻隊列中去

4.SDL創建音頻渲染線程,去音頻流隊列中取數據,進行解碼,輸出到聲卡

5.視頻渲染,由主線程實現,通過從解碼后的視頻流隊列中取得數據,進行渲染

以上使得音視頻的渲染都是通過回調的方式處理,使得更好的進行音視頻同步處理!!!

(二)基於多線程實現音頻數據的音視頻播放器 

#include <assert.h>
#include <stdio.h>
#include <SDL.h>

#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>

#define SDL_AUDIO_BUFFER_SIZE 1024        //對於AAC,一幀數據中單通道獲取 1024個 sample采樣點
#define    MAX_AUDIO_FRAME_SIZE 192000        //是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s

#define MAX_AUDIOQ_SIZE (5*6*1024)        //音頻隊列中數據字節數    界限,在MAX_AUDIO_FRAME_SIZE內部,即可。 采用最小8k采樣率,2通道,16位深 = 32k,這里使用(5*6*1024)小於這個最小值即可
#define MAX_VEDIOQ_SIZE (5*256*1024)    //視頻隊列中數據字節數    界限,需要處理1280x1024的圖像,這里使用這個數值作為界限,實際上對於yuv420p數據,可以在×1.5,但是沒必要的。

#define FF_REFRESH_EVENT (SDL_USEREVENT)    //使用自定義事件
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)    //使用自定義事件

#define VIDEO_PICTURE_QUEUE_SIZE 1        //定義視頻圖像裁剪后幀的最大個數

typedef struct PacketQueue {
    AVPacketList* first_pkt,* last_pkt;
    int nb_packets;
    int size;
    SDL_mutex* mutex;
    SDL_cond* cond;
}PacketQueue;

typedef struct VideoPicture {
    AVPicture* pict;    //存放yuv數據
    int width,height;
    int allocated;        //用來標記圖像是否分配了空間
}VideoPicture;

typedef struct VideoState {
    /*------------多媒體文件的基礎信息-----------*/
    char                     filename[1024];                //文件名
    AVFormatContext*         pFormatCtx;                    //文件格式上下文
    int                        videoStream,audioStream;    //視頻流、音頻流索引

    /*----------------音頻信息----------------*/
    AVStream                 *audio_st;                    //
    AVCodecContext             *audio_ctx;                    //解碼上下文
    struct SwrContext*         audio_swr_cxt;                //音頻重采樣上下文


    uint8_t                    audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];    //重采樣后的數據最后存放位置
    unsigned int             audio_buf_size;                //寫入的數據大小
    unsigned int             audio_buf_index;            //讀取到的數據的索引

    PacketQueue             audioq;                        //音頻隊列

    AVFrame                 audio_frame;                //音頻幀(解碼后的)--- 從隊列中取出來的packet,進行解碼出來的一幀數據
    AVPacket                 audio_pkt;                    //音頻包(隊列中讀取的),這里是提前初始化了一個包,避免了后面每次獲取數據時都設置一個,類似於全局變量了。或者我們像前一個版本那樣使用靜態局部變量也可以的
    uint8_t                    *audio_pkt_data;            //指向packet中的data數據要讀取的位置
    int                        audio_pkt_size;                //讀取的packet中的數據大小

    /*----------------視頻信息----------------*/
    AVStream                *video_st;                    //
    AVCodecContext            *video_ctx;                    //圖像解碼上下文
    struct SwsContext        *sws_ctx;                    //圖像裁剪上下文

    PacketQueue                videoq;                        //隊列中保存着所有讀取的關於視頻的packet

    VideoPicture             pictq[VIDEO_PICTURE_QUEUE_SIZE];    //最后要存放的數據位置---隊列中取出來的一個packet進行裁剪處理后的數據
    int                        pictq_size;                    //pictq_size表示pictq數組的大小,
    int                     pictq_rindex;                //索引pictq_rindex表示要訪問的數據在pictq數組中的位置
    int                     pictq_windex;                //索引pictq_windex表示要插入的數據在pictq數組中的位置

    /*----------------線程信息----------------*/            //只有視頻是我們要處理的數據,所以包含獨立的鎖,音頻是被聲卡主動讀取的
    SDL_mutex                *pictq_mutex;                //視頻數據的鎖,因為數據可以同時讀取和寫入
    SDL_cond                *pictq_cond;                //條件變量

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

    /*--------------全局退出信號---------------*/
    int                        quit;                        //0正常,1退出
}VideoState;


/*-------------------SDL相關-----------------*/
SDL_mutex                *texture_mutex;                    //是對渲染器的鎖,因為主線程和視頻解碼線程都會用到

SDL_Window                *win;                            //窗口指針
SDL_Renderer            *renderer;                        //渲染器指針
SDL_Texture                *texture;                        //紋理指針

/*--------------只有一個解復用線程,只在這里線程中被修改!!!---------------*/
VideoState                *global_video_state;            

/*-----------------隊列操作方法---------------*/
void packet_queue_init(PacketQueue *q){
    memset(q,0,sizeof(PacketQueue));
    q->mutex = SDL_CreateMutex();
    q->cond = SDL_CreateCond();
}

int packet_queue_get(PacketQueue *q,AVPacket *pkt,int block){
    AVPacketList *pktl;
    int ret;

    SDL_LockMutex(q->mutex);
    for(;;){
        if(global_video_state->quit){    //對於每個死循環,我們都要進行退出信號判斷
            fprintf(stderr,"quit from queue_get!\n");
            ret = -1;
            break;
        }

        pktl = q->first_pkt;
        if(pktl){
            q->first_pkt = pktl->next;
            if(!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pktl->pkt.size;
            *pkt = pktl->pkt;
            av_free(pktl);
            ret = 1;
            break;
        }else if(!block){
            ret = 0;    //表示沒有獲取到數據,接着進入輪詢
            break;
        }else{    //類似阻塞等待
            fprintf(stderr,"queue is empty, so wait a moment and wait a cond signal!\n");
            SDL_CondWait(q->cond,q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

int packet_queue_put(PacketQueue *q,AVPacket *pkt){
    AVPacketList* pktl;

    if(av_dup_packet(pkt) < 0){
        return -1;
    }

    pktl = av_malloc(sizeof(AVPacketList));
    if(!pktl){
        return -1;
    }

    pktl->pkt = *pkt;    //data賦值引用即可
    pktl->next = NULL;

    SDL_LockMutex(q->mutex);
    if(!q->last_pkt)
        q->first_pkt = pktl;
    else
        q->last_pkt->next = pktl;
    q->last_pkt = pktl;
    q->nb_packets++;
    q->size += pktl->pkt.size;

    SDL_CondSignal(q->cond);    //發送信號給get消費者

    SDL_UnlockMutex(q->mutex);
    return 0;
}

/*-----------------音頻操作方法---------------*/
int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size){
    int len1, data_size = 0;
    AVPacket *pkt = &is->audio_pkt;

    for(;;){
        while(is->audio_pkt_size > 0){    //還有數據在packet中
            int got_frame = 0;
            len1 = avcodec_decode_audio4(is->audio_ctx,&is->audio_frame,&got_frame,pkt);    //讀取pkt中的數據到frame
            if(len1 < 0){
                printf("Failed to decode audio...\n");
                is->audio_pkt_size = 0;
                break;
            }

            data_size = 0;
            if(got_frame){    //解碼幀成功
                data_size = 2*2*is->audio_frame.nb_samples;    //雙通道,16位深
                assert(data_size <= buf_size);

                //開始進行重采樣
                swr_convert(is->audio_swr_cxt,
                    &audio_buf,    //輸出空間
                    MAX_AUDIO_FRAME_SIZE*3/2,    //空間大小
                    (const uint8_t **)is->audio_frame.data,    //幀數據位置
                    is->audio_frame.nb_samples);    //幀通道采樣率

                //處理數據
                is->audio_pkt_size -= len1;
                is->audio_pkt_data += len1;
            }
            if(data_size == 0)    //沒有讀取到數據
                continue;
            return data_size;    //讀取到了數據
        }

        //如果沒有進入上面的while循環,則表示開始沒有數據存在pkt中,我們需要去隊列中獲取
        if(pkt->data)
            av_free_packet(pkt);

        if(is->quit){    //對於循環,一定退出判斷
            printf("Will quit program...\n");
            return -1;
        }

        if(packet_queue_get(&is->audioq,pkt,0) <= 0){
            return -1;
        }

        is->audio_pkt_data = pkt->data;
        is->audio_pkt_size = pkt->size;
    }
}

//聲卡回調讀取數據
void audio_callback(void *userdata,uint8_t *stream,int len){
    VideoState *is = (VideoState *)userdata;
    int len1,audio_size;

    SDL_memset(stream,0,len);    //初始化緩沖區

    while(len > 0){    //這個循環不用擔心,因為會產生靜默聲音,最后跳出while
        if(is->audio_buf_index >= is->audio_buf_size){    //原有的數據已經讀完了,下面開始獲取新的數據
            audio_size = audio_decode_frame(is,is->audio_buf,sizeof(is->audio_buf));
            if(audio_size < 0){    //讀取出錯 設置靜默聲音,當其中audio_decode_frame去隊列取數據block為0時,會進入
                is->audio_buf_size = 1024*2*2;
                printf("--------------no audio packet-------------------\n");
                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;
        //memcpy(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1);    //使用這個可以代替SDL_memset和
        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;
    }
}

/*-----------------視頻操作方法---------------*/
//設置定時器
static uint32_t sdl_refresh_timer_cb(uint32_t interval, void *dat){
    SDL_Event event;

    event.type = FF_REFRESH_EVENT;
    event.user.data1 = dat;
    SDL_PushEvent(&event);
    return 0;    //0意味着停止計時
}

//定時刷新
static void schedule_refresh(VideoState *is,int delay){
    SDL_AddTimer(delay,sdl_refresh_timer_cb,is);
}

//顯示圖像
void video_display(VideoState *is){
    SDL_Rect rect;
    VideoPicture *vp;

    vp = &is->pictq[is->pictq_rindex];    //獲取要顯示的數據
    if(vp->pict){    //有數據,開始顯示
        SDL_UpdateYUVTexture(texture,NULL,
            vp->pict->data[0],vp->pict->linesize[0],
            vp->pict->data[1],vp->pict->linesize[1],
            vp->pict->data[2],vp->pict->linesize[2]);

        rect.x = 0;
        rect.y = 0;
        rect.w = is->video_ctx->width;
        rect.h = is->video_ctx->height;

        SDL_LockMutex(texture_mutex);    //分配數據和渲染數據加鎖處理
        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer,texture,NULL,&rect);
        SDL_RenderPresent(renderer);
        SDL_UnlockMutex(texture_mutex);
    }
}

//開始根據事件處理顯示圖像
void video_refresh_timer(void *userdata){
    VideoState *is = (VideoState*)userdata;
    VideoPicture *vp;

    if(is->video_st){    //如果視頻流存在,則進行處理
        if(is->pictq_size == 0){    //隊列中沒有數據,則反復去輪詢是否有數據
            schedule_refresh(is,1);    //輪詢時間為1
        }else{
            vp = &is->pictq[is->pictq_rindex];    //獲取要顯示的數據

            schedule_refresh(is,40);    //顯示圖像之間間隔40ms
            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);    //通知queue_picture去分配空間
            SDL_UnlockMutex(is->pictq_mutex);
        }
    }else{
        schedule_refresh(is,100);    //如果不存在視頻流,就再延長等待一段時間
    }
}

//為pictq數組中新插入的數據分配空間
void alloc_picture(void *userdata){
    VideoState *is = (VideoState *)userdata;
    VideoPicture *vp = &is->pictq[is->pictq_windex];

    if(vp->pict){
        avpicture_free(vp->pict);    //釋放yuv空間
        free(vp->pict);    //釋放pict
    }

    SDL_LockMutex(texture_mutex);    //因為分配和渲染都要使用到這個數據,所以要進行加鎖
    vp->pict = (AVPicture*)malloc(sizeof(AVPicture));    //分配空間
    if(vp->pict){
        avpicture_alloc(vp->pict,
            AV_PIX_FMT_YUV420P,
            is->video_ctx->width,
            is->video_ctx->height);
    }
    SDL_UnlockMutex(texture_mutex);

    vp->width = is->video_ctx->width;
    vp->height = is->video_ctx->height;
    vp->allocated = 1;
}

//分配空間,將視頻幀放入pictq數組中去
int queue_picture(VideoState *is,AVFrame *pFrame){
    VideoPicture *vp;
    int dst_pix_fmt;
    AVPicture pict;    //yuv數據

    SDL_LockMutex(is->pictq_mutex);        //------------??應該是if
    while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit){    //表示隊列的大小已經到達最大界限(使用==判斷也可以)
        SDL_CondWait(is->pictq_cond,is->pictq_mutex);    //現在數組中已經放不下裁剪后的圖像了,只有主線程消費了圖像以后,發生信號過來,才進行解碼下一幀圖像
    }
    SDL_UnlockMutex(is->pictq_mutex);

    //上面的條件等待完成,表示前一個圖像顯示完成,現在需要裁剪新的圖像
    if(is->quit){
        printf("quit from queue picture...\n");
        return -1;
    }

    vp = &is->pictq[is->pictq_windex];    //這個是要插入的位置數據
    if(!vp->pict || 
        vp->width != is->video_ctx->width ||
        vp->height != is->video_ctx->height){    //開始分配空間
        vp->allocated = 0;        //還沒有開始分配空間
        alloc_picture(is);        //開始分配空間,內部會修改標識符
        if(is->quit){
            printf("quit from queue picture 2...\n");
            return -1;
        }
    }

    if(vp->pict){    //分配成功,開始進行裁剪處理,處理后的數據放入這里面
        sws_scale(is->sws_ctx,(uint8_t const* const*)pFrame->data,
            pFrame->linesize,0,is->video_ctx->height,
            vp->pict->data,vp->pict->linesize);

        if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE){
            is->pictq_windex = 0;
        }
        SDL_LockMutex(is->pictq_mutex);
        is->pictq_size++;    //多線程會操作這個
        SDL_UnlockMutex(is->pictq_mutex);
    }
    return 0;
}

//解碼視頻幀
int video_thread(void *arg){
    VideoState *is = (VideoState *)arg;
    AVPacket packet,*pkt = &packet;
    int frameFinished;
    AVFrame *pFrame;

    pFrame = av_frame_alloc();

    for(;;){
        if(packet_queue_get(&is->videoq,pkt,1) < 0){
            break;    //沒有數據,退出
        }

        avcodec_decode_video2(is->video_ctx,pFrame,&frameFinished,pkt);    //解碼數據幀
        if(frameFinished){    //去裁剪處理
            if(queue_picture(is,pFrame) < 0){
                break;
            }
        }

        av_free_packet(pkt);
    }
    av_frame_free(&pFrame);
    return 0;
}

/*-----------------解復用線程方法,主要用於初始化---------------*/

//初始化音視頻流的相關數據,(只被音頻、視頻流各自調用一次)
int stream_component_open(VideoState *is, int stream_index){
    int64_t in_channel_layout,out_channel_layout;
    AVFormatContext *pFormatCtx = is->pFormatCtx;
    AVCodecContext *codecCtx = NULL;
    AVCodec *codec = NULL;
    SDL_AudioSpec new_spec,old_spec;

    codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);    //獲取解碼器信息
    if(!codec){
        printf("UnSupport codec!\n");
        return -1;
    }

    codecCtx = avcodec_alloc_context3(codec);
    if(!codecCtx){
        printf("Failed to alloc codec context\n");
        return -1;
    }

    if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){
        printf("Failed to copy codec context\n");
        return -1;
    }

    if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO){    //設置音頻信息
        new_spec.freq = codecCtx->sample_rate;
        new_spec.format = AUDIO_S16SYS;
        new_spec.channels = codecCtx->channels;
        new_spec.silence = 0;
        new_spec.samples = SDL_AUDIO_BUFFER_SIZE;    //AAC單通道采樣率就是1024
        new_spec.callback = audio_callback;
        new_spec.userdata = is;

        if(SDL_OpenAudio(&new_spec,&old_spec)<0){
            printf("SDL_OpenAudio: %s\n", SDL_GetError());
            return -1;
        }
    }

    if(avcodec_open2(codecCtx,codec,NULL) < 0){
        printf("Failed to open codec\n");
        return -1;
    }

    switch(codecCtx->codec_type){
        case AVMEDIA_TYPE_AUDIO:    //處理音頻
            is->audioStream = stream_index;
            is->audio_st = pFormatCtx->streams[stream_index];
            is->audio_ctx = codecCtx;
            is->audio_buf_size = 0;
            is->audio_buf_index = 0;

            memset(&is->audio_pkt,0,sizeof(is->audio_pkt));    //取代了前一個版本的靜態局部變量而已,這里不使用memset也可以
            packet_queue_init(&is->audioq);

            SDL_PauseAudio(0);

            in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels);
            out_channel_layout = in_channel_layout;

            is->audio_swr_cxt = swr_alloc();    //開始分配重采樣的數據空間
            swr_alloc_set_opts(is->audio_swr_cxt,
                out_channel_layout,
                AV_SAMPLE_FMT_S16,
                is->audio_ctx->sample_rate,
                in_channel_layout,
                is->audio_ctx->sample_fmt,
                is->audio_ctx->sample_rate,
                0,
                NULL);

            swr_init(is->audio_swr_cxt);
            break;
        case AVMEDIA_TYPE_VIDEO:    //處理視頻
            is->videoStream = stream_index;
            is->video_st = pFormatCtx->streams[stream_index];
            is->video_ctx = codecCtx;
            packet_queue_init(&is->videoq);
            is->sws_ctx = sws_getContext(is->video_ctx->width,
                is->video_ctx->height,
                is->video_ctx->pix_fmt,
                is->video_ctx->width,
                is->video_ctx->height,
                AV_PIX_FMT_YUV420P,
                SWS_BILINEAR,
                NULL,NULL,NULL);
            is->video_tid = SDL_CreateThread(video_thread,"video_thread",is);    //開始視頻解碼線程
            break;
        default:
            break;
    }
    return 0;
}

//解復用,會在后面一直讀取視頻數據包
int decode_thread(void *arg){
    VideoState *is = (VideoState *)arg;
    AVFormatContext *pFormatCtx = NULL;
    AVPacket packet, *ptk = &packet;
    SDL_Event event;

    int i,ret=0;
    int video_index = -1;
    int audio_index = -1;

    //-------初始化操作
    is->videoStream = -1;
    is->audioStream = -1;

    global_video_state = is;

    //ffmpeg操作,獲取文件信息
    if(avformat_open_input(&pFormatCtx,is->filename,NULL,NULL)<0){
        printf("Failed to open file[%s] context\n", is->filename);
        return -1;
    }

    is->pFormatCtx = pFormatCtx;

    if(avformat_find_stream_info(pFormatCtx,NULL)<0){
        printf("Failed to get detail stream infomation\n");
        return -1;
    }

    for(i=0;i<pFormatCtx->nb_streams;i++){
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0)
            video_index = i;
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0)
            audio_index = i;
    }

    if(video_index < 0 || audio_index < 0){
        printf("Failed to find stream index for audio/video\n");
        return -1;
    }

    av_dump_format(pFormatCtx,audio_index,is->filename,0);    //打印信息
    av_dump_format(pFormatCtx,video_index,is->filename,0);    //打印信息

    //初始化音頻流信息
    ret = stream_component_open(is,audio_index);
    if(ret < 0){
        printf("Failed to initialize audio data\n");
        goto fail;
    }
    //初始化視頻流信息
    ret = stream_component_open(is,video_index);
    if(ret < 0){
        printf("Failed to initialize video data\n");
        goto fail;
    }

    //初始化SDL相關數據
    win = SDL_CreateWindow("Media Player",
        SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,
        is->video_ctx->width,is->video_ctx->height,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE
        );

    renderer = SDL_CreateRenderer(win,-1,0);

    texture = SDL_CreateTexture(renderer,
        SDL_PIXELFORMAT_IYUV,
        SDL_TEXTUREACCESS_STREAMING,
        is->video_ctx->width,
        is->video_ctx->height);

    //開始獲取包
    for(;;){
        if(is->quit){    //對於死循環,需要一直去判斷退出信號
            SDL_CondSignal(is->videoq.cond);    //既然要退出了,就要讓所有在循環中的程序退出,發送所有可能阻塞的信號量,防止因為缺少信號量而導致的阻塞
            SDL_CondSignal(is->audioq.cond);
            break;
        }

        if(is->audioq.size > MAX_AUDIOQ_SIZE ||
            is->videoq.size > MAX_VEDIOQ_SIZE){    //對於數據量大於界限值的時候,我們直接去獲取這些值,不去獲取下一個packet
            SDL_Delay(10);    //等待10ms
            continue;
        }

        if(av_read_frame(is->pFormatCtx,ptk) < 0){
            //context對象中有一個pb,這是內部用到的IO上下文,通過pb->error,就能獲取到IO上發生的錯誤。
            if(is->pFormatCtx->pb->error == 0){
                SDL_Delay(100);    //IO錯誤,等待一段時間用戶輸入
                continue;
            }else{    //讀取完畢,退出
                break;
            }
        }

        //開始處理讀取的數據包
        if(ptk->stream_index == is->videoStream){    //視頻包
            packet_queue_put(&is->videoq,ptk);        //當我們讀取的視頻也是25fps的話,播放聲音不會卡頓,當是30fps,音頻會出現卡頓,因為隊列中數據可能不存在
            printf("put video queue, size:%d\n", is->videoq.nb_packets);
        }else if(ptk->stream_index == is->audioStream){    //音頻包
            packet_queue_put(&is->audioq,ptk);
            printf("put audio queue, size:%d\n", is->audioq.nb_packets);
        }else{
            av_free_packet(ptk);
        }
    }

    while(!is->quit){    //出現數據包讀取完畢,放入了隊列中,但是還沒有從隊列中讀取完成去解碼播放,所以我們需要等待一段時間
        SDL_Delay(100);    //播放完畢以后,等待用戶主動關閉
    }

fail:
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);

    return 0;
}

/*-----------------主(函數)線程方法---------------*/
int main(int argc,char *argv[]){
    int ret = -1;

    SDL_Event event;    //主函數線程主要處理事件和視頻渲染問題
    VideoState *is;

    if(argc<2){
        fprintf(stderr,"Usage: test <file>\n");
        return ret;
    }

    //------進行初始化操作(簡單的初始化,初始化鎖、條件變量等信息),開啟解復用線程
    is = av_malloc(sizeof(VideoState));

    av_register_all();
    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
        printf("Failed to initialize SDL - %s\n", SDL_GetError());
        return ret;
    }
    texture_mutex = SDL_CreateMutex();

    av_strlcpy(is->filename,argv[1],sizeof(is->filename));

    is->pictq_mutex = SDL_CreateMutex();
    is->pictq_cond = SDL_CreateCond();

    schedule_refresh(is,40);    //設置40ms發送刷新信號,這個間隔內去實現解復用,獲取數據。如果這個時間不夠,后面獲取數據為空,則還會再延遲的

    //開啟解復用線程,內部也實現了很多初始化操作
    is->parse_tid = SDL_CreateThread(decode_thread,"decode_thread",is);
    if(!is->parse_tid){
        av_free(is);    //解復用線程開啟失敗
        goto __FAIL;
    }

    for(;;){
        SDL_WaitEvent(&event);    //阻塞式等待信號
        switch(event.type){
            case FF_QUIT_EVENT:    //出錯導致出現這個事件
            case SDL_QUIT:        //用戶點擊了關閉
                printf("receive a QUIT event:%d\n", event.type);
                is->quit = 1;    //其他線程推出
                goto __QUIT;
                break;
            case FF_REFRESH_EVENT:
                video_refresh_timer(event.user.data1);    //傳遞的數據就是VideoState
                break;
            default:
                break;
        }
    }

__QUIT:
    ret = 0;

__FAIL:
    SDL_Quit();
    return ret;
}
View Code
gcc playerV3.c -o play_3 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil -lswresample

因為音頻的頻率是25fps,視頻這里是30fps;所以是不同步的。而我們代碼中實現的是40ms每張圖片(25fps顯示),會導致視頻隊列中的數據包個數不斷累加!!!

四:簡單的播放器V4(線程實現播放視頻和音頻,實現音視頻同步)

(一)時間同步

回顧:FFmpeg學習(四)視頻基礎

補充1:PTS的獲取

一般使用解碼后的AVFrame的pts,但是pts出錯的話,可以通過函數獲取進行推算!!!

補充2:如何獲取下一幀的PTS

video_clock = 上一幀的video_clock + frame_delay幀間隔時間

音視頻同步就是這兩種clock相互追趕實現的!!

補充3:音視頻同步方式

1.視頻同步到音頻:視頻展示的時候進行延遲即可
2.音頻同步到視頻:丟幀或者添加靜默聲
3.都同步到系統時鍾

補充4:視頻播放的基本思路

(二)基於時間同步的音視頻播放器 

以audio為參考時鍾,video同步到音頻的示例代碼:

1.獲取當前要顯示的video PTS,減去上一幀視頻PTS,則得出上一幀視頻應該顯示的時長delay;
2.當前video PTS與參考時鍾當前audio PTS比較,得出音視頻差距diff;
3.獲取同步閾值sync_threshold,為一幀視頻差距,范圍為10ms-100ms;
4.diff小於sync_threshold,則認為不需要同步;否則delay+diff值,則是正確糾正delay;
5.如果超過sync_threshold,且視頻落后於音頻,那么需要減小delay(FFMAX(0, delay + diff)),讓當前幀盡快顯示。
6.如果視頻落后超過1秒,且之前10次都快速輸出視頻幀,那么需要反饋給音頻源減慢,同時反饋視頻源進行丟幀處理,讓視頻盡快追上音頻。因為這很可能是視頻解碼跟不上了,再怎么調整delay也沒用。
7.如果超過sync_threshold,且視頻快於音頻,那么需要加大delay,讓當前幀延遲顯示。
8.將delay*2慢慢調整差距,這是為了平緩調整差距,因為直接delay+diff,會讓畫面畫面遲滯。
9.如果視頻前一幀本身顯示時間很長,那么直接delay+diff一步調整到位,因為這種情況再慢慢調整也沒太大意義。
10.考慮到渲染的耗時,還需進行調整。frame_timer為一幀顯示的系統時間,frame_timer+delay- curr_time,則得出正在需要延遲顯示當前幀的時間。
#include <assert.h>
#include <stdio.h>
#include <math.h>
#include <SDL.h>

#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>

#define SDL_AUDIO_BUFFER_SIZE 1024        //對於AAC,一幀數據中單通道獲取 1024個 sample采樣點
#define    MAX_AUDIO_FRAME_SIZE 192000        //是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s

#define AV_SYNC_THRESHOLD 0.01            //10ms --- 同步閾值
#define AV_NOSYNC_THRESHOLD 10.0        //10s  --- 非同步閾值

#define MAX_AUDIOQ_SIZE (5*6*1024)        //音頻隊列中數據字節數    界限,在MAX_AUDIO_FRAME_SIZE內部,即可。 采用最小8k采樣率,2通道,16位深 = 32k,這里使用(5*6*1024)小於這個最小值即可
#define MAX_VEDIOQ_SIZE (5*256*1024)    //視頻隊列中數據字節數    界限,需要處理1280x1024的圖像,這里使用這個數值作為界限,實際上對於yuv420p數據,可以在×1.5,但是沒必要的。

#define FF_REFRESH_EVENT (SDL_USEREVENT)    //使用自定義事件
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)    //使用自定義事件

#define VIDEO_PICTURE_QUEUE_SIZE 1        //定義視頻圖像裁剪后幀的最大個數

typedef struct PacketQueue {
    AVPacketList* first_pkt,* last_pkt;
    int nb_packets;
    int size;
    SDL_mutex* mutex;
    SDL_cond* cond;
}PacketQueue;

typedef struct VideoPicture {
    AVPicture* pict;    //存放yuv數據
    int width,height;
    int allocated;        //用來標記圖像是否分配了空間
    double pts;
}VideoPicture;

typedef struct VideoState {
    /*------------多媒體文件的基礎信息-----------*/
    char                     filename[1024];                //文件名
    AVFormatContext*         pFormatCtx;                    //文件格式上下文
    int                        videoStream,audioStream;    //視頻流、音頻流索引

    /*----------------音頻信息----------------*/
    AVStream                 *audio_st;                    //
    AVCodecContext             *audio_ctx;                    //解碼上下文
    struct SwrContext*         audio_swr_cxt;                //音頻重采樣上下文


    uint8_t                    audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];    //重采樣后的數據最后存放位置
    unsigned int             audio_buf_size;                //寫入的數據大小
    unsigned int             audio_buf_index;            //讀取到的數據的索引

    PacketQueue             audioq;                        //音頻隊列

    AVFrame                 audio_frame;                //音頻幀(解碼后的)--- 從隊列中取出來的packet,進行解碼出來的一幀數據
    AVPacket                 audio_pkt;                    //音頻包(隊列中讀取的),這里是提前初始化了一個包,避免了后面每次獲取數據時都設置一個,類似於全局變量了。或者我們像前一個版本那樣使用靜態局部變量也可以的
    uint8_t                    *audio_pkt_data;            //指向packet中的data數據要讀取的位置
    int                        audio_pkt_size;                //讀取的packet中的數據大小

    /*----------------視頻信息----------------*/
    AVStream                *video_st;                    //
    AVCodecContext            *video_ctx;                    //圖像解碼上下文
    struct SwsContext        *sws_ctx;                    //圖像裁剪上下文

    PacketQueue                videoq;                        //隊列中保存着所有讀取的關於視頻的packet

    VideoPicture             pictq[VIDEO_PICTURE_QUEUE_SIZE];    //最后要存放的數據位置---隊列中取出來的一個packet進行裁剪處理后的數據
    int                        pictq_size;                    //pictq_size表示pictq數組的大小,
    int                     pictq_rindex;                //索引pictq_rindex表示要訪問的數據在pictq數組中的位置
    int                     pictq_windex;                //索引pictq_windex表示要插入的數據在pictq數組中的位置

    /*----------------線程信息----------------*/            //只有視頻是我們要處理的數據,所以包含獨立的鎖,音頻是被聲卡主動讀取的
    SDL_mutex                *pictq_mutex;                //視頻數據的鎖,因為數據可以同時讀取和寫入
    SDL_cond                *pictq_cond;                //條件變量

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

    /*--------------音視頻同步信息-------------*/
    double                    audio_clock;                //音頻(參考)時鍾當前audio PTS,時間戳
    double                    video_clock;                //視頻當前要顯示的video PTS

    double                    frame_timer;                //記錄下次回調的時間(是av_gettime獲取的系統時間,不是時間間隔)
    double                    frame_last_pts;                //上一次播放視頻幀的pts
    double                    frame_last_delay;            //上一次播放視頻幀的delay(也可以認為是上一個圖片顯示的時間)


    /*--------------全局退出信號---------------*/
    int                        quit;                        //0正常,1退出
}VideoState;


/*-------------------SDL相關-----------------*/
SDL_mutex                *texture_mutex;                    //是對渲染器的鎖,因為主線程和視頻解碼線程都會用到

SDL_Window                *win;                            //窗口指針
SDL_Renderer            *renderer;                        //渲染器指針
SDL_Texture                *texture;                        //紋理指針

/*--------------只有一個解復用線程,只在這里線程中被修改!!!---------------*/
VideoState                *global_video_state;            

/*-----------------隊列操作方法---------------*/
void packet_queue_init(PacketQueue *q){
    memset(q,0,sizeof(PacketQueue));
    q->mutex = SDL_CreateMutex();
    q->cond = SDL_CreateCond();
}

int packet_queue_get(PacketQueue *q,AVPacket *pkt,int block){
    AVPacketList *pktl;
    int ret;

    SDL_LockMutex(q->mutex);
    for(;;){
        if(global_video_state->quit){    //對於每個死循環,我們都要進行退出信號判斷
            fprintf(stderr,"quit from queue_get!\n");
            ret = -1;
            break;
        }

        pktl = q->first_pkt;
        if(pktl){
            q->first_pkt = pktl->next;
            if(!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pktl->pkt.size;
            *pkt = pktl->pkt;
            av_free(pktl);
            ret = 1;
            break;
        }else if(!block){
            ret = 0;    //表示沒有獲取到數據,接着進入輪詢
            break;
        }else{    //類似阻塞等待
            fprintf(stderr,"queue is empty, so wait a moment and wait a cond signal!\n");
            SDL_CondWait(q->cond,q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

int packet_queue_put(PacketQueue *q,AVPacket *pkt){
    AVPacketList* pktl;

    if(av_dup_packet(pkt) < 0){
        return -1;
    }

    pktl = av_malloc(sizeof(AVPacketList));
    if(!pktl){
        return -1;
    }

    pktl->pkt = *pkt;    //data賦值引用即可
    pktl->next = NULL;

    SDL_LockMutex(q->mutex);
    if(!q->last_pkt)
        q->first_pkt = pktl;
    else
        q->last_pkt->next = pktl;
    q->last_pkt = pktl;
    q->nb_packets++;
    q->size += pktl->pkt.size;

    SDL_CondSignal(q->cond);    //發送信號給get消費者

    SDL_UnlockMutex(q->mutex);
    return 0;
}

/*-----------------音頻操作方法---------------*/
//獲取音頻的時間戳---獲取方法和函數audio_decode_frame中的pts獲取相對
double get_audio_clock(VideoState *is){
    double pts;
    int hw_buf_size,bytes_per_sec,n;

    pts = is->audio_clock;        //獲取預測的下一個音頻包開始播放時間(或者當前音頻包的結束時間)
    hw_buf_size = is->audio_buf_size - is->audio_buf_index;    //這個就是當前播放的音頻大小
    bytes_per_sec = 0;
    n = is->audio_ctx->channels*2;    //通道數×2字節(16位深)
    if(is->audio_st){
        bytes_per_sec = is->audio_ctx->sample_rate*n;    //每秒所處理音頻的字節數量
    }
    if(bytes_per_sec){
        pts -= (double)hw_buf_size/bytes_per_sec;        //獲取當前音頻幀播放的時間!!!
    }
    return pts;
}


//音頻解碼
int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr){
    int len1, data_size = 0,n;
    AVPacket *pkt = &is->audio_pkt;
    double pts;

    for(;;){
        while(is->audio_pkt_size > 0){    //還有數據在packet中
            int got_frame = 0;
            len1 = avcodec_decode_audio4(is->audio_ctx,&is->audio_frame,&got_frame,pkt);    //讀取pkt中的數據到frame
            if(len1 < 0){
                printf("Failed to decode audio...\n");
                is->audio_pkt_size = 0;
                break;
            }

            data_size = 0;
            if(got_frame){    //解碼幀成功
                data_size = 2*2*is->audio_frame.nb_samples;    //雙通道,16位深
                assert(data_size <= buf_size);

                //開始進行重采樣
                swr_convert(is->audio_swr_cxt,
                    &audio_buf,    //輸出空間
                    MAX_AUDIO_FRAME_SIZE*3/2,    //空間大小
                    (const uint8_t **)is->audio_frame.data,    //幀數據位置
                    is->audio_frame.nb_samples);    //幀通道采樣率

                //處理數據
                is->audio_pkt_size -= len1;
                is->audio_pkt_data += len1;
            }
            if(data_size == 0)    //沒有讀取到數據
                continue;
            //---------------------------------------重點:更新音頻包的時間戳!!------------------------------------
            pts = is->audio_clock;    //獲取時間戳
            *pts_ptr = pts;            //賦值
            n = 2*is->audio_ctx->channels;    //通道×2字節(重采樣后的16位深) 

            //(n*is->audio_ctx->sample_rate)是1s內采樣的數據量,所以
            //(double)data_size / (double)(n*is->audio_ctx->sample_rate)就是這些數據處理(播放)會花的時間
            is->audio_clock += (double)data_size / (double)(n*is->audio_ctx->sample_rate);    //就相當於下一次的音頻pts,因為部分packet不會包含pts,這樣推斷更好
            //這個is->audio_clock是預測的下一次時間(或者本次音頻包播放完畢的時間)

            return data_size;    //讀取到了數據
        }

        //如果沒有進入上面的while循環,則表示開始沒有數據存在pkt中,我們需要去隊列中獲取
        if(pkt->data)
            av_free_packet(pkt);

        if(is->quit){    //對於循環,一定退出判斷
            printf("Will quit program...\n");
            return -1;
        }

        if(packet_queue_get(&is->audioq,pkt,0) <= 0){
            return -1;
        }

        is->audio_pkt_data = pkt->data;
        is->audio_pkt_size = pkt->size;

        //---------------------------------------重點:獲取音頻包的時間戳!!------------------------------------
        if(pkt->pts != AV_NOPTS_VALUE){    //如果這個數據包有PTS值,就開始更新
            is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;    //獲取時間戳!! timestamp(秒) = pts * av_q2d(time_base)
        }
    }
}

//聲卡回調讀取數據
void audio_callback(void *userdata,uint8_t *stream,int len){
    VideoState *is = (VideoState *)userdata;
    int len1,audio_size;
    double pts;            //返回的當前的音頻幀的pts!!!

    SDL_memset(stream,0,len);    //初始化緩沖區

    while(len > 0){    //這個循環不用擔心,因為會產生靜默聲音,最后跳出while
        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){    //讀取出錯 設置靜默聲音,當其中audio_decode_frame去隊列取數據block為0時,會進入
                is->audio_buf_size = 1024*2*2;
                printf("--------------no audio packet-------------------\n");
                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;
        //memcpy(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1);    //使用這個可以代替SDL_memset和
        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;
    }
}

/*-----------------視頻操作方法---------------*/
//設置定時器
static uint32_t sdl_refresh_timer_cb(uint32_t interval, void *dat){
    SDL_Event event;

    event.type = FF_REFRESH_EVENT;
    event.user.data1 = dat;
    SDL_PushEvent(&event);
    return 0;    //0意味着停止計時
}

//定時刷新
static void schedule_refresh(VideoState *is,int delay){
    SDL_AddTimer(delay,sdl_refresh_timer_cb,is);
}

//顯示圖像
void video_display(VideoState *is){
    SDL_Rect rect;
    VideoPicture *vp;

    vp = &is->pictq[is->pictq_rindex];    //獲取要顯示的數據
    if(vp->pict){    //有數據,開始顯示
        SDL_UpdateYUVTexture(texture,NULL,
            vp->pict->data[0],vp->pict->linesize[0],
            vp->pict->data[1],vp->pict->linesize[1],
            vp->pict->data[2],vp->pict->linesize[2]);

        rect.x = 0;
        rect.y = 0;
        rect.w = is->video_ctx->width;
        rect.h = is->video_ctx->height;

        SDL_LockMutex(texture_mutex);    //分配數據和渲染數據加鎖處理
        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer,texture,NULL,&rect);
        SDL_RenderPresent(renderer);
        SDL_UnlockMutex(texture_mutex);
    }
}

//開始根據事件處理顯示圖像
void video_refresh_timer(void *userdata){
    VideoState *is = (VideoState*)userdata;
    VideoPicture *vp;
    /*
    actual_delay:    實際時延間隔
    delay:             上一次的時間
    sync_threshold  同步閾值
    ref_clock:        參考時間(音頻的時間)
    diff:            差值=當前視頻幀pts - 音頻的參考pts
    */
    double actual_delay,delay,sync_threshold,ref_clock,diff;

    if(is->video_st){                                //如果視頻流存在,則進行處理
        if(is->pictq_size == 0){                    //隊列中沒有數據,則反復去輪詢是否有數據
            schedule_refresh(is,1);                    //輪詢時間為1
        }else{
            vp = &is->pictq[is->pictq_rindex];        //獲取要顯示的數據

            delay = vp->pts - is->frame_last_pts;    //當前視頻幀的pts - 上一幀的pts
            if(delay <= 0 || delay >= 1.0){            //不合理時間 0或者1s
                delay = is->frame_last_delay;        //上一次的時延(展示時間)
            }

            is->frame_last_delay = delay;            //更新本次時延
            is->frame_last_pts = vp->pts;            //上一幀的pts = 當前的pts

            ref_clock = get_audio_clock(is);        //獲取參考幀的數據
            diff = vp->pts - ref_clock;

            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;    //delay時間是否超過閾值,進行更新(取大)
            if(fabs(diff) < AV_NOSYNC_THRESHOLD){    //非同步閾值10s
                if(diff <= -sync_threshold){        //視頻比音頻慢(diff為負號),並且超過允許的時延閾值(<=-sync_threshold),加快----
                    delay = 0;    //快點播放
                }else if(diff >= sync_threshold){    //視頻比音頻快(diff為正號),並且超過允許的時延閾值
                    delay *= 2;                        //視頻延遲展示
                }
            }

            is->frame_timer += delay;                //下次回調時間(時間戳)
            actual_delay = is->frame_timer - (av_gettime()/1000000.0);    //去計算時延間隔(delay)---秒

            if(actual_delay < AV_SYNC_THRESHOLD){    //在允許的閾值范圍內
                actual_delay = AV_SYNC_THRESHOLD;    //至少10ms刷新一次
            }

            schedule_refresh(is,(int)(actual_delay*1000+0.5));    //下一次幀渲染時間,將上面獲取的s轉換為ms,+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);    //通知queue_picture去分配空間
            SDL_UnlockMutex(is->pictq_mutex);
        }
    }else{
        schedule_refresh(is,100);    //如果不存在視頻流,就再延長等待一段時間
    }
}

//為pictq數組中新插入的數據分配空間
void alloc_picture(void *userdata){
    VideoState *is = (VideoState *)userdata;
    VideoPicture *vp = &is->pictq[is->pictq_windex];

    if(vp->pict){
        avpicture_free(vp->pict);    //釋放yuv空間
        free(vp->pict);    //釋放pict
    }

    SDL_LockMutex(texture_mutex);    //因為分配和渲染都要使用到這個數據,所以要進行加鎖
    vp->pict = (AVPicture*)malloc(sizeof(AVPicture));    //分配空間
    if(vp->pict){
        avpicture_alloc(vp->pict,
            AV_PIX_FMT_YUV420P,
            is->video_ctx->width,
            is->video_ctx->height);
    }
    SDL_UnlockMutex(texture_mutex);

    vp->width = is->video_ctx->width;
    vp->height = is->video_ctx->height;
    vp->allocated = 1;
}

//分配空間,將視頻幀放入pictq數組中去
int queue_picture(VideoState *is,AVFrame *pFrame,double pts){
    VideoPicture *vp;
    int dst_pix_fmt;
    AVPicture pict;    //yuv數據

    SDL_LockMutex(is->pictq_mutex);        //------------??應該是if
    while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit){    //表示隊列的大小已經到達最大界限(使用==判斷也可以)
        SDL_CondWait(is->pictq_cond,is->pictq_mutex);    //現在數組中已經放不下裁剪后的圖像了,只有主線程消費了圖像以后,發生信號過來,才進行解碼下一幀圖像
    }
    SDL_UnlockMutex(is->pictq_mutex);

    //上面的條件等待完成,表示前一個圖像顯示完成,現在需要裁剪新的圖像
    if(is->quit){
        printf("quit from queue picture...\n");
        return -1;
    }

    vp = &is->pictq[is->pictq_windex];    //這個是要插入的位置數據
    if(!vp->pict || 
        vp->width != is->video_ctx->width ||
        vp->height != is->video_ctx->height){    //開始分配空間
        vp->allocated = 0;        //還沒有開始分配空間
        alloc_picture(is);        //開始分配空間,內部會修改標識符
        if(is->quit){
            printf("quit from queue picture 2...\n");
            return -1;
        }
    }

    if(vp->pict){    //分配成功,開始進行裁剪處理,處理后的數據放入這里面
        //---------------------修改pts---------------------
        vp->pts = pts;    //更新pts

        sws_scale(is->sws_ctx,(uint8_t const* const*)pFrame->data,
            pFrame->linesize,0,is->video_ctx->height,
            vp->pict->data,vp->pict->linesize);

        if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE){
            is->pictq_windex = 0;
        }
        SDL_LockMutex(is->pictq_mutex);
        is->pictq_size++;    //多線程會操作這個
        SDL_UnlockMutex(is->pictq_mutex);
    }
    return 0;
}

double synchronize_video(VideoState* is,AVFrame *src_frame,double pts){
    double frame_delay;

    if(pts != 0){
        is->video_clock = pts;    //直接更新video_clock
    }else{
        pts = is->video_clock;    //否則使用上一次的video_clock作為pts值
    }

    frame_delay = av_q2d(is->video_ctx->time_base);    //通過時間基獲取幀間隔

    //糾正play (播放時間)的方法 repeat_pict / (2 * fps) 是ffmpeg注釋里教的
    frame_delay += src_frame->repeat_pict * (frame_delay*0.5);    //這個幀有一個字段repeat_pict,是要這個幀重復的播放
    is->video_clock += frame_delay;    //保存了下一幀的pts

    return pts;
}

//解碼視頻幀
int video_thread(void *arg){
    VideoState *is = (VideoState *)arg;
    AVPacket packet,*pkt = &packet;
    int frameFinished;
    AVFrame *pFrame;
    double pts;

    pFrame = av_frame_alloc();

    for(;;){
        if(packet_queue_get(&is->videoq,pkt,1) < 0){
            break;    //沒有數據,退出
        }

        avcodec_decode_video2(is->video_ctx,pFrame,&frameFinished,pkt);    //解碼數據幀

        //------------------------獲取視頻幀的pts信息------------------------
        if((pts = av_frame_get_best_effort_timestamp(pFrame)) == AV_NOPTS_VALUE){    //av_frame_get_best_effort_timestamp計算最合適的pts時間
            pts = 0;
        }
        pts *= av_q2d(is->video_st->time_base);        //獲取時間戳信息---轉換為秒

        if(frameFinished){    //去裁剪處理
            pts = synchronize_video(is,pFrame,pts);    //計算pts,防止前面的pts為0,然后順便獲取下一幀的pts
            if(queue_picture(is,pFrame,pts) < 0){    //內部修改視頻的pts信息
                break;
            }
        }

        av_free_packet(pkt);
    }
    av_frame_free(&pFrame);
    return 0;
}

/*-----------------解復用線程方法,主要用於初始化---------------*/

//初始化音視頻流的相關數據,(只被音頻、視頻流各自調用一次)
int stream_component_open(VideoState *is, int stream_index){
    int64_t in_channel_layout,out_channel_layout;
    AVFormatContext *pFormatCtx = is->pFormatCtx;
    AVCodecContext *codecCtx = NULL;
    AVCodec *codec = NULL;
    SDL_AudioSpec new_spec,old_spec;

    codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);    //獲取解碼器信息
    if(!codec){
        printf("UnSupport codec!\n");
        return -1;
    }

    codecCtx = avcodec_alloc_context3(codec);
    if(!codecCtx){
        printf("Failed to alloc codec context\n");
        return -1;
    }

    if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){
        printf("Failed to copy codec context\n");
        return -1;
    }

    if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO){    //設置音頻信息
        new_spec.freq = codecCtx->sample_rate;
        new_spec.format = AUDIO_S16SYS;
        new_spec.channels = codecCtx->channels;
        new_spec.silence = 0;
        new_spec.samples = SDL_AUDIO_BUFFER_SIZE;    //AAC單通道采樣率就是1024
        new_spec.callback = audio_callback;
        new_spec.userdata = is;

        if(SDL_OpenAudio(&new_spec,&old_spec)<0){
            printf("SDL_OpenAudio: %s\n", SDL_GetError());
            return -1;
        }
    }

    if(avcodec_open2(codecCtx,codec,NULL) < 0){
        printf("Failed to open codec\n");
        return -1;
    }

    switch(codecCtx->codec_type){
        case AVMEDIA_TYPE_AUDIO:    //處理音頻
            is->audioStream = stream_index;
            is->audio_st = pFormatCtx->streams[stream_index];
            is->audio_ctx = codecCtx;
            is->audio_buf_size = 0;
            is->audio_buf_index = 0;

            memset(&is->audio_pkt,0,sizeof(is->audio_pkt));    //取代了前一個版本的靜態局部變量而已,這里不使用memset也可以
            packet_queue_init(&is->audioq);

            SDL_PauseAudio(0);

            in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels);
            out_channel_layout = in_channel_layout;

            is->audio_swr_cxt = swr_alloc();    //開始分配重采樣的數據空間
            swr_alloc_set_opts(is->audio_swr_cxt,
                out_channel_layout,
                AV_SAMPLE_FMT_S16,
                is->audio_ctx->sample_rate,
                in_channel_layout,
                is->audio_ctx->sample_fmt,
                is->audio_ctx->sample_rate,
                0,
                NULL);

            swr_init(is->audio_swr_cxt);
            break;
        case AVMEDIA_TYPE_VIDEO:    //處理視頻
            is->videoStream = stream_index;
            is->video_st = pFormatCtx->streams[stream_index];
            is->video_ctx = codecCtx;

            //------------------------時間初始化------------------------
            is->frame_timer = (double)av_gettime()/1000000.0;
            is->frame_last_delay = 40e-3;

            packet_queue_init(&is->videoq);
            is->sws_ctx = sws_getContext(is->video_ctx->width,
                is->video_ctx->height,
                is->video_ctx->pix_fmt,
                is->video_ctx->width,
                is->video_ctx->height,
                AV_PIX_FMT_YUV420P,
                SWS_BILINEAR,
                NULL,NULL,NULL);
            is->video_tid = SDL_CreateThread(video_thread,"video_thread",is);    //開始視頻解碼線程
            break;
        default:
            break;
    }
    return 0;
}

//解復用,會在后面一直讀取視頻數據包
int decode_thread(void *arg){
    VideoState *is = (VideoState *)arg;
    AVFormatContext *pFormatCtx = NULL;
    AVPacket packet, *ptk = &packet;
    SDL_Event event;

    int i,ret=0;
    int video_index = -1;
    int audio_index = -1;

    //-------初始化操作
    is->videoStream = -1;
    is->audioStream = -1;

    global_video_state = is;

    //ffmpeg操作,獲取文件信息
    if(avformat_open_input(&pFormatCtx,is->filename,NULL,NULL)<0){
        printf("Failed to open file[%s] context\n", is->filename);
        return -1;
    }

    is->pFormatCtx = pFormatCtx;

    if(avformat_find_stream_info(pFormatCtx,NULL)<0){
        printf("Failed to get detail stream infomation\n");
        return -1;
    }

    for(i=0;i<pFormatCtx->nb_streams;i++){
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0)
            video_index = i;
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0)
            audio_index = i;
    }

    if(video_index < 0 || audio_index < 0){
        printf("Failed to find stream index for audio/video\n");
        return -1;
    }

    av_dump_format(pFormatCtx,audio_index,is->filename,0);    //打印信息
    av_dump_format(pFormatCtx,video_index,is->filename,0);    //打印信息

    //初始化音頻流信息
    ret = stream_component_open(is,audio_index);
    if(ret < 0){
        printf("Failed to initialize audio data\n");
        goto fail;
    }
    //初始化視頻流信息
    ret = stream_component_open(is,video_index);
    if(ret < 0){
        printf("Failed to initialize video data\n");
        goto fail;
    }

    //初始化SDL相關數據
    win = SDL_CreateWindow("Media Player",
        SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,
        is->video_ctx->width,is->video_ctx->height,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE
        );

    renderer = SDL_CreateRenderer(win,-1,0);

    texture = SDL_CreateTexture(renderer,
        SDL_PIXELFORMAT_IYUV,
        SDL_TEXTUREACCESS_STREAMING,
        is->video_ctx->width,
        is->video_ctx->height);

    //開始獲取包
    for(;;){
        if(is->quit){    //對於死循環,需要一直去判斷退出信號
            SDL_CondSignal(is->videoq.cond);    //既然要退出了,就要讓所有在循環中的程序退出,發送所有可能阻塞的信號量,防止因為缺少信號量而導致的阻塞
            SDL_CondSignal(is->audioq.cond);
            break;
        }

        if(is->audioq.size > MAX_AUDIOQ_SIZE ||
            is->videoq.size > MAX_VEDIOQ_SIZE){    //對於數據量大於界限值的時候,我們直接去獲取這些值,不去獲取下一個packet
            SDL_Delay(10);    //等待10ms
            continue;
        }

        if(av_read_frame(is->pFormatCtx,ptk) < 0){
            //context對象中有一個pb,這是內部用到的IO上下文,通過pb->error,就能獲取到IO上發生的錯誤。
            if(is->pFormatCtx->pb->error == 0){
                SDL_Delay(100);    //IO錯誤,等待一段時間用戶輸入
                continue;
            }else{    //讀取完畢,退出
                break;
            }
        }

        //開始處理讀取的數據包
        if(ptk->stream_index == is->videoStream){    //視頻包
            packet_queue_put(&is->videoq,ptk);        //當我們讀取的視頻也是25fps的話,播放聲音不會卡頓,當是30fps,音頻會出現卡頓,因為隊列中數據可能不存在
            printf("put video queue, size:%d\n", is->videoq.nb_packets);
        }else if(ptk->stream_index == is->audioStream){    //音頻包
            packet_queue_put(&is->audioq,ptk);
            printf("put audio queue, size:%d\n", is->audioq.nb_packets);
        }else{
            av_free_packet(ptk);
        }
    }

    while(!is->quit){    //出現數據包讀取完畢,放入了隊列中,但是還沒有從隊列中讀取完成去解碼播放,所以我們需要等待一段時間
        SDL_Delay(100);    //播放完畢以后,等待用戶主動關閉
    }

fail:
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);

    return 0;
}

/*-----------------主(函數)線程方法---------------*/
int main(int argc,char *argv[]){
    int ret = -1;

    SDL_Event event;    //主函數線程主要處理事件和視頻渲染問題
    VideoState *is;

    if(argc<2){
        fprintf(stderr,"Usage: test <file>\n");
        return ret;
    }

    //------進行初始化操作(簡單的初始化,初始化鎖、條件變量等信息),開啟解復用線程
    is = av_malloc(sizeof(VideoState));

    av_register_all();
    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
        printf("Failed to initialize SDL - %s\n", SDL_GetError());
        return ret;
    }
    texture_mutex = SDL_CreateMutex();

    av_strlcpy(is->filename,argv[1],sizeof(is->filename));

    is->pictq_mutex = SDL_CreateMutex();
    is->pictq_cond = SDL_CreateCond();

    schedule_refresh(is,40);    //設置40ms發送刷新信號,這個間隔內去實現解復用,獲取數據。如果這個時間不夠,后面獲取數據為空,則還會再延遲的

    //開啟解復用線程,內部也實現了很多初始化操作
    is->parse_tid = SDL_CreateThread(decode_thread,"decode_thread",is);
    if(!is->parse_tid){
        av_free(is);    //解復用線程開啟失敗
        goto __FAIL;
    }

    for(;;){
        SDL_WaitEvent(&event);    //阻塞式等待信號
        switch(event.type){
            case FF_QUIT_EVENT:    //出錯導致出現這個事件
            case SDL_QUIT:        //用戶點擊了關閉
                printf("receive a QUIT event:%d\n", event.type);
                is->quit = 1;    //其他線程推出
                goto __QUIT;
                break;
            case FF_REFRESH_EVENT:
                video_refresh_timer(event.user.data1);    //傳遞的數據就是VideoState
                break;
            default:
                break;
        }
    }

__QUIT:
    ret = 0;

__FAIL:
    SDL_Quit();
    return ret;
}
View Code

因為進行了音視頻同步,所以導致音頻、視頻隊列中的數據包個數幾乎保持不變,為12和37!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM