1. 程式人生 > 程式設計 >ffmpeg播放器實現詳解之框架搭建過程

ffmpeg播放器實現詳解之框架搭建過程

ffplay是ffmpeg原始碼中一個自帶的開源播放器例項,同時支援本地視訊檔案的播放以及線上流媒體播放,功能非常強大。

FFplay: FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs.

ffplay中的程式碼充分呼叫了ffmpeg中的函式庫,因此,想學習ffmpeg的使用,或基於ffmpeg開發一個自己的播放器,ffplay都是一個很好的切入點。

由於ffmpeg本身的開發文件比較少,且ffplay播放器原始碼的實現相對複雜,除了基礎的ffmpeg元件呼叫外,還包含視訊幀的渲染、音訊幀的播放、音視訊同步策略及執行緒排程等問題。

因此,這裡我們以ffmpeg官網推薦的一個ffplay播放器簡化版本的開發例程為基礎,在此基礎上循序漸進由淺入深,最終探討實現一個視訊播放器的完整邏輯。

ffplay播放器簡化版本開發例程可在ffmpeg官網[documentation]頁面的右下角找到,點選An FFmpeg and SDL Tutorial即可開啟找到對應的原始碼。

ffmpeg播放器實現詳解之框架搭建過程

1、專案編譯環境搭建

這裡仍以Ubuntu 16.04 LTS為基礎進行講述,由於ffmpeg支援多個主流平臺,且api介面在各個平臺是一致的,因此其他平臺也可參照本文內容,後續會將程式碼移植到windows等其他平臺,方便大家除錯。

原始碼的編譯除了ffmpeg環境外,還需要SDL-1.x版本的支援,用於提供視訊幀的渲染及音訊幀的播放。

1.1 sdl庫編譯

SDL(Simple DirectMedia Layer)是一個跨平臺的多媒體和遊戲開發包,提供2D,音訊,事件驅動,多執行緒和定時器等服務,它使用C語言寫成,提供了多種控制影象、聲音、輸出的函式,讓開發者只要用相同或是相似的程式碼就可以開發出跨多個平臺(Linux、Windows、Mac OS X等)的應用軟體。

SDL: Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio,keyboard,mouse,joystick,and graphics hardware via OpenGL and Direct3D. It is used by video playback software,emulators,and popular games including Valve's award winning catalog and many Humble Bundle games.

可通過下面的連結下載SDL-1.2.15原始碼,注意,例程中依賴的SDL版本與ffplay中有所不同

https://www.libsdl.org/download-1.2.php

下載完成後解壓進入sdl原始碼目錄,可通過下面的配置方法生成Makefile檔案

./configure --prefix=/usr/local/3rdparty/sdl

生成Makefile檔案後,輸入make命令即可開始編譯過程,編譯完成後,執行make install命令進行安裝

make 
make install

安裝完成後,會在configure指定的目錄下找到sdl的目錄,由於sdl以庫檔案的方式提供支援,因此在sdl/bin目錄下沒有對應的可執行檔案。

1.2 sdl環境變數配置

sdl編譯完成後,還需要讓系統能夠找到對應的安裝位置。開啟/etc/profile配置檔案,在該檔案底部新增sdl的環境變數

#SDL ENVIRONMENT
export C_INCLUDE_PATH=/usr/local/3rdparty/sdl/include/SDL:$C_INCLUDE_PATH
export LD_LIBRARY_PATH=/usr/local/3rdparty/sdl/lib:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=/usr/local/3rdparty/sdl/lib/pkgconfig:$PKG_CONFIG_PATH

1.3 專案原始碼編譯

專案原始碼可採用如下Makefile指令碼進行編譯

tutorial01: tutorial01.c
	gcc -o tutorial01 -g3 tutorial01.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} \
	-L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
	`sdl-config --cflags --libs`

clean:
	rm -rf tutorial01
	rm -rf *.ppm

執行make命令開始編譯,編譯完成後,可在原始碼目錄生成名為[tutorial01]的可執行檔案。

1.4 驗證

與ffplay的使用方法類似,執行[tutorial01 url]命令,可以看到在原始碼目錄生成的字尾名為.ppm的影象

./tutorial01 rtmp://58.200.131.2:1935/livetv/hunantv

ppm影象在linux平臺下可直接開啟,看到有ppm影象生成,即可確定專案能夠正常工作,輸入Ctrl+C結束程式執行。

ffmpeg播放器實現詳解之框架搭建過程

ppm格式的影象平時不太常用,大家沒有必要做深入研究,這裡僅用於對編譯結果的驗證。

PPM: A PPM file is a 24-bit color image formatted using a text format. It stores each pixel with a number from 0 to 65536,which specifies the color of the pixel. PPM files also store the image height and width,whitespace data,and the maximum color value. The portable pixmap format (PPM),the portable graymap format (PGM) and the portable bitmap format (PBM) are image file formats designed to be easily exchanged between platforms.

2 原始碼分析

上述例程除了生成幾張圖片外,好像什麼也做不了,似乎離一個功能完整的視訊播放器還有很遠的距離。

儘管如此,例程依然包含了ffmpeg視訊開發用到的幾乎所有關鍵的api與資料結構。後面的內容會在此基礎上不斷的完善,直至實現一個完整的視訊播放器。

2.1 流程

下面給出例程的流程圖,流程非常簡單,所有程式碼都執行在主執行緒中,流程涉及api及資料結構的含義都在例程原始碼中有詳細的註釋。

ffmpeg播放器實現詳解之框架搭建過程

2.2 原始碼中涉及的api及元件

由於篇幅的限制,這裡先簡要介紹每個元件及api的含義,後續文章中會深入介紹每個元件及api的使用方法

元件:

  • AVFormatContext 儲存檔案容器封裝資訊及碼流引數的結構體
  • AVCodecContext 解碼器上下文物件,解碼器依賴的相關環境、狀態、資源以及引數集的介面指標
  • AVCodec 儲存編解碼器資訊的結構體,提供編碼與解碼的公共介面
  • AVPacket 負責儲存壓縮編碼資料相關資訊的結構體,每幀影象由一到多個packet包組成
  • AVFrame 儲存音視訊解碼後的資料,如狀態資訊、編解碼器資訊、巨集塊型別表,QP表,運動矢量表等資料
  • SwsContext 描述轉換器引數的結構體

api :

  • av_register_all 註冊所有ffmpeg支援的多媒體格式及編解碼器
  • avformat_open_input 開啟視訊檔案,讀檔案頭內容,取得檔案容器的封裝資訊及碼流引數並存儲在pFormatCtx中
  • avformat_find_stream_info 取得檔案中儲存的碼流資訊,並填充到pFormatCtx->stream 欄位
  • avcodec_find_decoder 根據視訊流對應的解碼器上下文查詢對應的解碼器,返回對應的解碼器
  • avcodec_alloc_context3 複製編解碼器上下文物件,用於儲存從視訊流中抽取的幀
  • avcodec_open2 開啟解碼器
  • av_frame_alloc 為解碼後的視訊資訊結構體分配空間並完成初始化操作
  • av_read_frame 從檔案中依次讀取每個影象編碼資料包,並存儲在AVPacket資料結構中
  • avcodec_decode_video2 解碼完整的一幀資料,若一個packet無法解碼一個完整的視訊幀,則在ffmpeg後臺維護的快取佇列會持續等待多個packet,直到能夠解碼出一個完整的視訊幀為止

3 ffmpeg能幫我們做什麼

視訊開發涉及到多種視訊格式的編解碼,多種檔案格式及傳輸協議的解封裝等操作,很難一下子全部掌握。

ffmpeg通過其封裝的api及元件,為我們遮蔽了不同視訊封裝格式及編碼格式的差異,以統一的api介面提供給開發者使用,開發者不需要了解每種編碼方式及封裝方式具體的技術細節,只需要呼叫ffmpeg提供的api就可以完成解封裝和解碼的操作了。

至於視訊幀的渲染及音訊幀的播放,ffmpeg就無能為力了,因此需要藉助類似sdl庫等其他元件完成,後面的章節會為大家介紹繼續介紹。

4 原始碼清單

// tutorial01.c
// Code based on a tutorial by Martin Bohme ([email protected])
// Tested on Gentoo,CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100,LAVF 54.29.104,LSWS 2.1.101 
// on GCC 4.7.2 in Debian February 2015
//
// Updates tested on:
// Mac OS X 10.11.6
// Apple LLVM version 8.0.0 (clang-800.0.38)
//
// A small sample program that shows how to use libavformat and libavcodec to read video from a file.
//
// Use
//
// $ gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lswscale -lz -lm
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).
//
// Run using
//
// $ tutorial01 myvideofile.mpg
//
// to write the first five frames from "myvideofile.mpg" to disk in PPM format.

// comment by [email protected]

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

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

//儲存PPM檔案
void SaveFrame(AVFrame *pFrame,int width,int height,int iFrame) {
	FILE *pFile;//定義檔案物件
	char szFilename[32];//定義輸出檔名
	
	// Open file,開啟檔案
	sprintf(szFilename,"frame%d.ppm",iFrame);//格式化輸出檔名
	pFile = fopen(szFilename,"wb");//開啟輸出檔案
	if (pFile == NULL) {//檢查輸出檔案是否開啟成功
		return;
	}
 
	// Write header indicated how wide & tall the image is,向輸出檔案中寫入檔案頭
	fprintf(pFile,"P6\n%d %d\n255\n",width,height);
 
	// Write pixel data,write the file one line a time,一次一行迴圈寫入RGB24畫素值
	int y;
	for (y = 0; y < height; y++) {
		fwrite(pFrame->data[0]+y*pFrame->linesize[0],1,width*3,pFile);
	}
 
	// Close file,關閉檔案
	fclose(pFile);
}

int main(int argc,char *argv[]) {
/*--------------引數定義-------------*/
	// Initalizing these to NULL prevents segfaults!
	AVFormatContext *pFormatCtx = NULL;//儲存檔案容器封裝資訊及碼流引數的結構體
	AVCodecContext *pCodecCtxOrig = NULL;//解碼器上下文物件,解碼器依賴的相關環境、狀態、資源以及引數集的介面指標
	AVCodecContext *pCodecCtx = NULL;//編碼器上下文物件,用於PPM檔案輸出
	AVCodec *pCodec = NULL;//儲存編解碼器資訊的結構體,提供編碼與解碼的公共介面,可以看作是編碼器與解碼器的一個全域性變數
	AVPacket packet;//負責儲存壓縮編碼資料相關資訊的結構體,每幀影象由一到多個packet包組成
	AVFrame *pFrame = NULL;//儲存音視訊解碼後的資料,如狀態資訊、編解碼器資訊、巨集塊型別表,QP表,運動矢量表等資料
	AVFrame *pFrameRGB = NULL;//儲存輸出24-bit RGB的PPM檔案資料
	struct SwsContext *sws_ctx = NULL;//描述轉換器引數的結構體

	int numBytes;//RGB24格式資料長度
	uint8_t *buffer = NULL;//解碼資料輸出快取指標
	int i,videoStream;//迴圈變數,視訊流型別標號
	int frameFinished;//解碼操作是否成功標識

/*-------------引數初始化------------*/
	if (argc<2) {//檢查輸入引數個數是否正確
		printf("Please provide a movie file\n");
		return -1;
	}

	// Register all available formats and codecs,註冊所有ffmpeg支援的多媒體格式及編解碼器
	av_register_all();

	/*-----------------------
	 * Open video file,開啟視訊檔案,讀檔案頭內容,取得檔案容器的封裝資訊及碼流引數並存儲在pFormatCtx中
	 * read the file header and stores information about the file format in the AVFormatContext structure 
	 * The last three arguments are used to specify the file format,buffer size,and format options
	 * but by setting this to NULL or 0,libavformat will auto-detect these
	 -----------------------*/
	if (avformat_open_input(&pFormatCtx,argv[1],NULL,NULL) != 0) {
		return -1; // Couldn't open file.
	}

	/*-----------------------
	 * 取得檔案中儲存的碼流資訊,並填充到pFormatCtx->stream 欄位
	 * check out & Retrieve the stream information in the file
	 * then populate pFormatCtx->stream with the proper information 
	 * pFormatCtx->streams is just an array of pointers,of size pFormatCtx->nb_streams
	 -----------------------*/
	if (avformat_find_stream_info(pFormatCtx,NULL) < 0) {
		return -1; // Couldn't find stream information.
	}

	// Dump information about file onto standard error,列印pFormatCtx中的碼流資訊
	av_dump_format(pFormatCtx,0);

	// Find the first video stream.
	videoStream=-1;//視訊流型別標號初始化為-1
	for (i=0;i<pFormatCtx->nb_streams;i++) {//遍歷檔案中包含的所有流媒體型別(視訊流、音訊流、字幕流等)
		if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {//若檔案中包含有視訊流
			videoStream = i;//用視訊流型別的標號修改標識,使之不為-1
			break;//退出迴圈
		}
	}
	if (videoStream==-1) {//檢查檔案中是否存在視訊流
		return -1; // Didn't find a video stream.
	}

	// Get a pointer to the codec context for the video stream,根據流型別標號從pFormatCtx->streams中取得視訊流對應的解碼器上下文
	pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;
	/*-----------------------
	 * Find the decoder for the video stream,根據視訊流對應的解碼器上下文查詢對應的解碼器,返回對應的解碼器(資訊結構體)
	 * The stream's information about the codec is in what we call the "codec context.
	 * This contains all the information about the codec that the stream is using
	 -----------------------*/
	pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
	if (pCodec == NULL) {//檢查解碼器是否匹配
		fprintf(stderr,"Unsupported codec!\n");
		return -1; // Codec not found.
	}

	// Copy context,複製編解碼器上下文物件,用於儲存從視訊流中抽取的幀
	pCodecCtx = avcodec_alloc_context3(pCodec);//建立AVCodecContext結構體物件pCodecCtx
	if (avcodec_copy_context(pCodecCtx,pCodecCtxOrig) != 0) {//複製編解碼器上下文物件
		fprintf(stderr,"Couldn't copy codec context");
		return -1; // Error copying codec context.
	}

	// Open codec,開啟解碼器
	if (avcodec_open2(pCodecCtx,pCodec,NULL) < 0) {
		return -1; // Could not open codec.
	}

	// Allocate video frame,為解碼後的視訊資訊結構體分配空間並完成初始化操作(結構體中的影象快取按照下面兩步手動安裝)
	pFrame = av_frame_alloc();

	// Allocate an AVFrame structure,為轉換PPM檔案的結構體分配空間並完成初始化操作
	pFrameRGB = av_frame_alloc();
	if (pFrameRGB == NULL) {//檢查初始化操作是否成功
		return -1;
	}

	// Determine required buffer size and allocate buffer,根據畫素格式及影象尺寸計算記憶體大小
	numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24,pCodecCtx->width,pCodecCtx->height,1);
	buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));//為轉換後的RGB24影象配置快取空間

	// Assign appropriate parts of buffer to image planes in pFrameRGB Note that pFrameRGB is an AVFrame,but AVFrame is a superset of AVPicture
	// 為AVFrame物件安裝影象快取,將out_buffer快取掛到pFrameYUV->data指標結構上
	av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize,buffer,AV_PIX_FMT_RGB24,1);
 
	// Initialize SWS context for software scaling,設定影象轉換畫素格式為AV_PIX_FMT_RGB24
	sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->pix_fmt,SWS_BILINEAR,NULL);

/*--------------迴圈解碼-------------*/
	i = 0;// Read frames(2 packet) and save first five frames to disk,/*-----------------------
	 * read in a packet and store it in the AVPacket struct
	 * ffmpeg allocates the internal data for us,which is pointed to by packet.data
	 * this is freed by the av_free_packet()
	 -----------------------*/
	while (av_read_frame(pFormatCtx,&packet) >= 0) {//從視訊檔案或網路流媒體中依次讀取每個影象編碼資料包,並存儲在AVPacket資料結構中
		// Is this a packet from the video stream,檢查資料包型別
		if (packet.stream_index == videoStream) {
		  /*-----------------------
	 		 * Decode video frame,解碼完整的一幀資料,並將frameFinished設定為true
			 * 可能無法通過只解碼一個packet就獲得一個完整的視訊幀frame,可能需要讀取多個packet才行
	 		 * avcodec_decode_video2()會在解碼到完整的一幀時設定frameFinished為真
			 * Technically a packet can contain partial frames or other bits of data
			 * ffmpeg's parser ensures that the packets we get contain either complete or multiple frames
			 * convert the packet to a frame for us and set frameFinisned for us when we have the next frame
	 	 	 -----------------------*/
			avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,&packet);

			// Did we get a video frame,檢查是否解碼出完整一幀影象
			if (frameFinished) {
				// Convert the image from its native format to RGB,//將解碼後的影象轉換為RGB24格式
				sws_scale(sws_ctx,(uint8_t const * const *) pFrame->data,pFrame->linesize,pFrameRGB->data,pFrameRGB->linesize);

				if (++i <= 5) {// Save the frame to disk,將前5幀影象儲存到磁碟上
					SaveFrame(pFrameRGB,i);
				}
			}
		}
		// Free the packet that was allocated by av_read_frame,釋放AVPacket資料結構中編碼資料指標
		av_packet_unref(&packet);
	}

/*--------------引數撤銷-------------*/
	// Free the RGB image buffer
	av_free(buffer);
	av_frame_free(&pFrameRGB);

	// Free the YUV frame.
	av_frame_free(&pFrame);

	// Close the codecs.
	avcodec_close(pCodecCtx);
	avcodec_close(pCodecCtxOrig);

	// Close the video file.
	avformat_close_input(&pFormatCtx);

	return 0;
}

到此這篇關於ffmpeg播放器實現詳解之 框架搭建過程的文章就介紹到這了,更多相關ffmpeg播放器框架搭建內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!