Android 基於ffmpeg開發簡易播放器 - ffmpeg解封裝

貓尾巴發表於2018-06-11

ffmpeg解封裝

需要呼叫ffmpeg的API首先需要引入對應的標頭檔案:

extern "C"{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
複製程式碼

1.初始化解封裝

//初始化解封裝
av_register_all();
//初始化網路,可以直接從伺服器拉流
avformat_network_init();
複製程式碼

av_register_all()用於註冊所有複用器,編碼器和協議處理器。如果要指定註冊某種編碼器可以使用:av_register_input_format() ,av_register_output_format(),ffurl_register_protocol()。av_register_all()呼叫了avcodec_register_all()。avcodec_register_all()註冊了和編解碼器有關的元件:硬體加速器,解碼器,編碼器,Parser,Bitstream Filter。av_register_all()除了呼叫avcodec_register_all()之外,還註冊了複用器,解複用器,協議處理器。

Android 基於ffmpeg開發簡易播放器 - ffmpeg解封裝
avformat_network_init()用於網路元件的全域性初始化。這是可選的,但建議使用,因為它避免了隱式地為每個會話進行安裝的開銷。如果在某些主要版本中使用網路協議,呼叫此函式將成為強制性要求。載入socket庫以及網路加密協議相關的庫,為後續使用網路相關提供支援。

2.開啟媒體檔案

//開啟檔案
AVFormatContext *ic = NULL;
char path[] = "/sdcard/1080.mp4";
int re = avformat_open_input(&ic,path,0,0);
if(re == 0)
{
   LOGW("avformat_open_input %s success!",path);
}
else
{
    LOGW("avformat_open_input failed!:%s",av_err2str(re));
}
複製程式碼

ffmpeg開啟媒體的的過程開始於avformat_open_input()。在該方法呼叫之前確保av_register_all(),avformat_network_init()已經被呼叫。該函式用於開啟多媒體資料(輸入流)並且獲得一些相關的資訊(頭資料)。對應的關閉流的函式為avformat_close_input()。

該方法中主要完成了:

  • 輸入輸出結構體AVIOContext的初始化;

  • 輸入資料的協議(例如RTMP,或者file)的識別(通過一套評分機制):1.判斷檔名的字尾 2.讀取檔案頭的資料進行比對;

  • 使用獲得最高分的檔案協議對應的URLProtocol,通過函式指標的方式,與FFMPEG連線(非專業用詞);

  • 剩下的就是呼叫該URLProtocol的函式進行open,read等操作了。

URLProtocol結構如下,是一大堆函式指標的集合(avio.h檔案)

typedef struct URLProtocol {  
    const char *name;  
    int (*url_open)(URLContext *h, const char *url, int flags);  
    int (*url_read)(URLContext *h, unsigned char *buf, int size);  
    int (*url_write)(URLContext *h, const unsigned char *buf, int size);  
    int64_t (*url_seek)(URLContext *h, int64_t pos, int whence);  
    int (*url_close)(URLContext *h);  
    struct URLProtocol *next;  
    int (*url_read_pause)(URLContext *h, int pause);  
    int64_t (*url_read_seek)(URLContext *h, int stream_index,  
                             int64_t timestamp, int flags);  
    int (*url_get_file_handle)(URLContext *h);  
    int priv_data_size;  
    const AVClass *priv_data_class;  
    int flags;  
    int (*url_check)(URLContext *h, int mask);  
} URLProtocol;  
複製程式碼

URLProtocol功能就是完成各種輸入協議的讀寫等操作。

該方法的簽名為:

int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);  
複製程式碼
  • ps:函式呼叫成功之後處理過的AVFormatContext結構體。
  • file:開啟的視音訊流的URL。
  • fmt:強制指定AVFormatContext中AVInputFormat的。這個引數一般情況下可以設定為NULL,這樣ffmpeg可以自動檢測AVInputFormat。
  • dictionay:附加的一些選項,一般情況下可以設定為NULL。

函式執行成功的話,其返回值大於等於0。

AVFormatContext:輸入資料的封裝格式

  • AVIOContext *pb:輸入資料的快取

  • unsigned int nb_streams:視音訊流的個數

  • AVStream **streams:視音訊流

  • char filename[1024]:檔名

  • int64_t duration:時長(單位:微秒us,轉換為秒需要除以1000000)

  • int bit_rate:位元率(單位bps,轉換為kbps需要除以1000)

  • AVDictionary *metadata:後設資料

  • char filename[1024]:輸入或輸出檔名

  • void avformat_close_input(AVFormatContext **s);:該函式用於關閉一個AVFormatContext,一般情況下是和avformat_open_input()成對使用的。

3.獲取流資訊

avformat_find_stream_info()。該函式可以讀取一部分視音訊資料並且獲得一些相關的資訊(適用於沒有頭部資訊的檔案):

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);  
複製程式碼
  • ic:輸入的AVFormatContext。
  • options:額外的選項。

函式正常執行後返回值大於等於0。

//獲取流資訊
re = avformat_find_stream_info(ic,0);
if(re != 0)
{
    LOGW("avformat_find_stream_info failed!");
}
LOGW("duration = %lld nb_streams = %d",ic->duration,ic->nb_streams);
複製程式碼

獲取音視訊資訊:

static double r2d(AVRational r)
{
    return r.num==0||r.den == 0 ? 0 :(double)r.num/(double)r.den;
}

int fps = 0;
int videoStream = 0;
int audioStream = 1;

for(int i = 0; i < ic->nb_streams; i++)
{
    AVStream *as = ic->streams[i];
    if(as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
    {
        LOGW("視訊資料");
        videoStream = i;
        fps = r2d(as->avg_frame_rate);

        LOGW("fps = %d,width=%d height=%d codeid=%d pixformat=%d",fps,
             as->codecpar->width,
             as->codecpar->height,
             as->codecpar->codec_id,
             as->codecpar->format
        );
    }
    else if(as->codecpar->codec_type ==AVMEDIA_TYPE_AUDIO )
    {
        LOGW("音訊資料");
        audioStream = i;
        LOGW("sample_rate=%d channels=%d sample_format=%d",
             as->codecpar->sample_rate,
             as->codecpar->channels,
             as->codecpar->format
        );
    }
}
複製程式碼

AVStream:儲存每一個視訊/音訊流資訊的結構體。

int index:標識該視訊/音訊流

AVCodecContext *codec:指向該視訊/音訊流的AVCodecContext(它們是一一對應的關係)。codec引數在58版本及之後就不會支援了,需要由codecpar引數所替代。

AVRational time_base:時基。通過該值可以把PTS,DTS轉化為真正的時間。FFMPEG其他結構體中也有這個欄位,但是根據我的經驗,只有AVStream中的time_base是可用的。PTS*time_base=真正的時間。

int64_t duration:該視訊/音訊流長度。

AVDictionary *metadata:後設資料資訊。

AVRational avg_frame_rate:幀率(注:對視訊來說,這個挺重要的)。

AVPacket attached_pic:附帶的圖片。比如說一些MP3,AAC音訊檔案附帶的專輯封面。

AVCodecParameters *codecpar:codec引數在58版本及之後就不會支援了,需要由codecpar引數所替代。

獲取音訊流索引:

//獲取音訊流資訊
audioStream = av_find_best_stream(ic,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
複製程式碼
int av_find_best_stream	(	
    AVFormatContext * 	ic,
    enum AVMediaType 	type,
    int 	wanted_stream_nb,
    int 	related_stream,
    AVCodec ** 	decoder_ret,
    int 	flags 
)	
複製程式碼

在檔案中找到“最佳”流。

ic:媒體檔案控制程式碼。

type:流型別:視訊,音訊,字幕等。

wanted_stream_nb:使用者請求的流號碼,或-1用於自動選擇。

related_stream:嘗試查詢與此相關的流(例如,在相同的程式中),如果沒有,則返回-1。

decoder_ret:如果非NULL,則返回所選流的解碼器。

flags:目前沒有定義。

4.讀取音視訊幀資料

//讀取幀資料
AVPacket *pkt = av_packet_alloc();
for(;;)
{
    int re = av_read_frame(ic,pkt);
    if(re != 0)
    {
        LOGW("讀取到結尾處!");
        int pos = 20 * r2d(ic->streams[videoStream]->time_base);
        av_seek_frame(ic,videoStream,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
        continue;
    }
    LOGW("stream = %d size =%d pts=%lld flag=%d",
         pkt->stream_index,pkt->size,pkt->pts,pkt->flags
    );
    av_packet_unref(pkt);
}
複製程式碼

AVPacket *av_packet_alloc(void):分配一個AVPacket結構體大小的記憶體。

void av_packet_unref(AVPacket *pkt):釋放對應的AVPacket結構體。

AVPacket是儲存壓縮編碼資料相關資訊的結構體。

uint8_t *data:壓縮編碼的資料。

例如對於H.264來說。1個AVPacket的data通常對應一個NAL。

注意:在這裡只是對應,而不是一模一樣。他們之間有微小的差別:使用FFMPEG類庫分離出多媒體檔案中的H.264碼流

因此在使用FFMPEG進行視音訊處理的時候,常常可以將得到的AVPacket的data資料直接寫成檔案,從而得到視音訊的碼流檔案。

int size:data的大小

int64_t pts:顯示時間戳(num/den)

int64_t dts:解碼時間戳

int stream_index:標識該AVPacket所屬的視訊/音訊流。

Android 基於ffmpeg開發簡易播放器 - ffmpeg解封裝

讀取幀資料:

int av_read_frame(AVFormatContext * s,AVPacket * pkt)
複製程式碼

返回流的下一幀。

此函式返回儲存在檔案中的內容,並且不驗證解碼器的有效幀是什麼。它會將儲存在檔案中的內容拆分為幀,併為每個呼叫返回一個。它不會忽略有效幀之間的無效資料,從而為解碼器提供解碼所需的最大資訊。

如果pkt-> buf為NULL,那麼資料包在下一個av_read_frame()或avformat_close_input()之前是有效的。否則資料包無限期地有效。在這兩種情況下,資料包必須在不再需要時使用av_free_packet釋放。對於視訊,資料包恰好包含一幀。對於音訊,如果每個幀具有已知的固定大小(例如PCM或ADPCM資料),則它包含整數個幀。如果音訊幀具有可變大小(例如MPEG音訊),則它包含一幀。

pkt-> pts,pkt-> dts和pkt->duration始終設定為以AVStream.time_base單位的正確值。如果視訊格式具有B幀,則pkt-> pts可以是AV_NOPTS_VALUE,所以如果不解壓縮有效載荷,則最好依賴pkt-> dts。

返回

如果成功返回為0,錯誤或檔案結束時為 < 0。

設定ffmpeg將流偏移到正確的起始位置:

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
複製程式碼

s:為容器內容;

stream_index:流索引

timestamp:將要定位處的時間戳

flags:功能flag

#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE     2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY      4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME    8 ///< seeking based on frame number
複製程式碼

相關文章