之前一直用Directshow技術采集攝像頭數據,但是覺得涉及的細節比較多,要開發者比較了解Directshow的框架知識,學習起來有一點點難度。最近發現很多人問怎么用FFmpeg采集攝像頭圖像,事實上FFmpeg很早就支持通過DShow獲取采集設備(攝像頭、麥克風)的數據了,只是網上提供的例子比較少。如果能用FFmpeg實現采集、編碼和錄制(或推流),那整個實現方案就簡化很多,正因為這個原因,我想嘗試做一個FFmpeg采集攝像頭視頻和麥克風音頻的程序。經過一個星期的努力,終於做出來了。我打算把開發的心得和經驗分享給大家。我分三部分來講述:首先第一部分介紹如何用FFmpeg的官方工具(ffmpeg.exe)通過命令行來枚舉DShow設備和采集攝像頭圖像,這部分是基礎,能夠快速讓大家熟悉怎么用FFmpeg測試攝像頭采集;第二部分介紹我寫的采集程序的功能和用法;第三部分講解各個模塊包括采集、編碼、封裝和錄制是如何實現的。
(該例子代碼的下載地址:http://download.csdn.net/download/zhoubotong2012/10252187)
1.用命令行枚舉采集設備和采集數據
打開Cmd命令行控制台,進入FFmpeg的Bin目錄,輸入如下命令:
ffmpeg -list_devices true -f dshow -i dummy
則在我的機器上顯示如下結果:

在上面的命令行窗口中列出了兩個設備,一個是視頻采集設備,另外是一個音頻采集設備。另外,我們發現:音頻設備的名稱有亂碼,因為其中有中文名稱,后面在講到用API采集數據的時候會提到解決這個問題的方法。
接着我們輸入另外一個命令行:
ffmpeg -list_options true -f dshow -i video="USB 2861 Device"
這個命令行的作用是獲取指定視頻采集設備支持的分辨率、幀率和像素格式等屬性,返回的是一個列表,結果如下:

這里我們看到采集設備支持的最大分辨率是720x576,輸出像素格式是yuyv422,支持的幀率為29.97和25FPS。
下面我們執行另外一條命令,將攝像頭的圖像和麥克風的音頻錄制保存成一個文件。命令如下:
ffmpeg -f dshow -i video="USB 2861 Device" -f dshow -i audio="線路 (3- USB Audio Device)" -vcodec libx264 -acodec aac -strict -2 mycamera.mkv
上面的命令行用video=指定視頻設備,用audio=指定音頻設備,后面的參數是定義編碼器的格式和屬性,輸出為一個名為mycamera.mkv的文件。
命令運行之后,控制台打印FFmpeg的運行日志,按“Q”鍵則中止命令。
這里有些讀者可能會問:采集設備不是支持多個分辨率嗎?怎么設置采集時用哪一種分辨率輸出?答案是用“-s”參數設置,若在上面的命令行加上“-s 720x576”,則FFmpeg就會以720x576的分辨率進行采集,如果不設置,則以默認的分辨率輸出。
注意:如果你運行上面命令ffmpeg報如下錯誤:Could not run filter
Video=XXX:Input/output error

則說明該版本的ffmpeg不支持該采集設備。這是由於舊版本的FFmpeg一個Bug引起的,不支持需要連接crossbar連接的視頻采集設備(詳情可參考這個帖子:https://ffmpeg.zeranoe.com/forum/viewtopic.php?t=722)。如果讀者運行下面的Demo遇到打開視頻采集設備失敗,可能也是這個問題引起的. 新版本的FFmpeg(avdevice-58)修復了這個問題。所以如果你遇到這個問題,可以通過升級FFmpeg來解決。
好,關於命令行的內容就介紹完了。
2.采集程序的使用
這個程序叫“AVCapture”,能從視頻采集設備(攝像頭,采集卡)獲取圖像,支持圖像預覽;還可以采集麥克風音頻;支持對視頻和音頻編碼,支持錄制成文件。這是一個MFC開發的窗口程序,界面比較簡潔,如下圖:

開始采集前需要選擇設備,點擊文件菜單的“打開設備”,彈出一個設備選擇對話框,如下圖所示:

在對話框里選擇任意一個視頻設備和音頻設備,如果想啟用某種設備,必須勾選右邊的“啟用”選項,但如果只需要用其中一種采集設備,則可以把其中一個禁用掉。
按“確定”則開始采集數據了。視頻和音頻會編碼后保存到一個文件中,這個文件的路徑是在配置文件中設置的,打開程序目錄下的Config.ini文件,則顯示如下字段:
[Client]
file_path = D:\camera.mkv
File_path就是錄制文件的路徑。
采集的圖像默認顯示到中間的窗口中,如果不想預覽,可以在主菜單欄的“編輯”菜單中取消勾選“預覽視頻”。
3.功能模塊實現
該采集程序實現了枚舉采集設備,采集控制、顯示圖像、視頻/音頻編碼和錄制的功能,其中輸入(Input)、輸出(Output)和顯示(Paint)這三個模塊分別用一個單獨的類進行封裝:CAVInputStream,CAVOutputStream,CImagePainter。CAVInputStream負責從采集設備獲取數據,提供接口獲取采集設備的屬性,以及提供回調函數把數據傳給上層。CAVOutputStream負責對采集的視頻和音頻流進行編碼、封裝,保存成一個文件。而CImagePainter則用來顯示圖像,使用了GDI繪圖,把圖像顯示到主界面的窗口。
3.1 枚舉采集設備
采集前我們需要先選擇設備,把所有的設備名稱列出來,其中一個方法可以用第一節介紹的運行ffmpeg命令行工具來列舉,但是這樣有兩個問題:第一,假如設備名稱帶中文,則顯示的名稱有亂碼,因此,我們不知道它真實的名稱。第二,ffmpeg沒有API返回系統中安裝的采集設備列表,雖然FFmpeg提供了API把設備名稱列舉出來,但是是打印到控制台的,不是通過參數來返回,如下面這段代碼只能打印輸出結果到控制台。但是對於窗口界面程序,沒有控制台,怎么獲取命令行結果呢?
AVFormatContext *pFmtCtx =
avformat_alloc_context();
AVDictionary* options =
NULL;
av_dict_set(&options,
"list_devices",
"true",
0);
AVInputFormat *iformat =
av_find_input_format(
"dshow");
avformat_open_input(&pFmtCtx,
"video=dummy", iformat, &options);
我用了一種最傳統的做法來解決,就是通過Directshow的COM接口來枚舉設備,工程里面的EnumDevice接口就實現了枚舉設備的功能,函數原型如下:
ENUMDEVICE_API HRESULT EnumDevice(CAPTURE_DEVICE_TYPE type, char * deviceList[], int nListLen, int & iNumCapDevices);
當然,如果讀者用的采集設備是固定一種,那么可以固定采集設備的名稱,這樣做可以省點事。
3.2 注冊FFmpeg庫
這兩個API可以在程序的構造函數和窗口初始化里面調用。
3.3 打開輸入設備
首先需要指定采集設備的名稱。如果是視頻設備類型,則名稱以“video=”開頭;如果是音頻設備類型,則名稱以“audio=”開頭。調用avformat_open_input接口打開設備,將設備名稱作為參數傳進去,注意這個設備名稱需要轉成UTF-8編碼。然后調用avformat_find_stream_info獲取流的信息,得到視頻流或音頻流的索引號,之后會頻繁用到這個索引號來定位視頻和音頻的Stream信息。接着,調用avcodec_open2打開視頻解碼器或音頻解碼器,實際上,我們可以把設備也看成是一般的文件源,而文件一般采用某種封裝格式,要播放出來需要進行解復用,分離成裸流,然后對單獨的視頻流、音頻流進行解碼。雖然采集出來的圖像或音頻都是未編碼的,但是按照FFmpeg的常規處理流程,我們需要加上“解碼”這個步驟。
m_pInputFormat =
av_find_input_format(
"dshow");
ASSERT(m_pInputFormat !=
NULL);
if(!m_video_device.
empty())
string device_name =
"video=" + m_video_device;
string device_name_utf8 =
AnsiToUTF8(device_name.
c_str(), device_name.
length());
if ((res =
avformat_open_input(&m_pVidFmtCtx, device_name_utf8.
c_str(), m_pInputFormat, &device_param)) !=
0)
ATLTRACE(
"Couldn't open input video stream.(無法打開輸入流)\n");
if (
avformat_find_stream_info(m_pVidFmtCtx,
NULL) <
0)
ATLTRACE(
"Couldn't find video stream information.(無法獲取流信息)\n");
for (i =
0; i < m_pVidFmtCtx->nb_streams; i++)
if (m_pVidFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
ATLTRACE(
"Couldn't find a video stream.(沒有找到視頻流)\n");
if (
avcodec_open2(m_pVidFmtCtx->streams[m_videoindex]->codec,
avcodec_find_decoder(m_pVidFmtCtx->streams[m_videoindex]->codec->codec_id),
NULL) <
0)
ATLTRACE(
"Could not open video codec.(無法打開解碼器)\n");
if(!m_audio_device.
empty())
string device_name =
"audio=" + m_audio_device;
string device_name_utf8 =
AnsiToUTF8(device_name.
c_str(), device_name.
length());
if (
avformat_open_input(&m_pAudFmtCtx, device_name_utf8.
c_str(), m_pInputFormat, &device_param) !=
0){
ATLTRACE(
"Couldn't open input audio stream.(無法打開輸入流)\n");
if (
avformat_find_stream_info(m_pAudFmtCtx,
NULL) <
0)
ATLTRACE(
"Couldn't find audio stream information.(無法獲取流信息)\n");
for (i =
0; i < m_pAudFmtCtx->nb_streams; i++)
if (m_pAudFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
ATLTRACE(
"Couldn't find a audio stream.(沒有找到音頻流)\n");
if (
avcodec_open2(m_pAudFmtCtx->streams[m_audioindex]->codec,
avcodec_find_decoder(m_pAudFmtCtx->streams[m_audioindex]->codec->codec_id),
NULL) <
0)
ATLTRACE(
"Could not open audio codec.(無法打開解碼器)\n");
3.4 初始化輸出流
前面我們已經初始化了InputStream,現在需要對OutputStream進行初始化,而要初始化輸出流需要知道視頻采集的分辨率,幀率,輸出像素格式等信息,還有音頻采集設備的采樣率,聲道數,Sample格式,而這些信息可通過CAVInputStream類的接口來獲取到。下面是初始化OutputStream的代碼:
m_InputStream.
SetVideoCaptureCB(VideoCaptureCallback);
m_InputStream.
SetAudioCaptureCB(AudioCaptureCallback);
bRet = m_InputStream.
OpenInputStream();
MessageBox(_T(
"打開采集設備失敗"), _T(
"提示"), MB_OK|MB_ICONWARNING);
if(m_InputStream.
GetVideoInputInfo(cx, cy, fps, pixel_fmt))
m_OutputStream.
SetVideoCodecProp(AV_CODEC_ID_H264, fps,
500000,
100, cx, cy);
int sample_rate =
0, channels =
0;
AVSampleFormat sample_fmt;
if(m_InputStream.
GetAudioInputInfo(sample_fmt, sample_rate, channels))
m_OutputStream.
SetAudioCodecProp(AV_CODEC_ID_AAC, sample_rate, channels,
32000);
P_GetProfileString(_T(
"Client"),
"file_path", m_szFilePath,
sizeof(m_szFilePath));
bRet = m_OutputStream.
OpenOutputStream(m_szFilePath);
MessageBox(_T(
"初始化輸出失敗"), _T(
"提示"), MB_OK|MB_ICONWARNING);
在上面的代碼片段中,首先設置了視頻和音頻的數據回調函數。當采集開始時,視頻和音頻數據就會傳遞給相應的函數去處理,在該程序中,回調函數主要對圖像或音頻進行編碼,然后封裝成FFmpeg支持的容器(例如mkv/avi/mpg/ts/mp4)。另外,需要初始化OutputStream的VideoCodec和AudioCodec的屬性,在我的程序中,視頻編碼器是H264,音頻編碼器用AAC,通過CAVInputStream對象獲得輸入流的信息之后再賦值給輸出流相應的參數。最后調用m_OutputStream對象的OpenOutputStream成員函數打開編碼器和錄制的容器,其中我們需要傳入一個輸出文件路徑作為參數,這個為錄制的文件路徑,路徑是在Config.ini文件里配置的。如果OpenOutputStream函數返回true,則表示初始化輸出流成功。
3.5 讀取采集數據
接着,我們就可以開始采集了。開始采集的函數實現如下:
bool CAVInputStream::StartCapture()
if (m_videoindex ==
-1 && m_audioindex ==
-1)
ATLTRACE(
"錯誤:你沒有打開設備\n");
m_start_time =
av_gettime();
if(!m_video_device.
empty())
m_hCapVideoThread =
CreateThread(
if(!m_audio_device.
empty())
m_hCapAudioThread =
CreateThread(
StartCapture函數分別建立了一個讀取視頻包和讀取音頻包的線程,兩個線程各自獨立工作,分別從視頻采集設備,音頻采集設備獲取到數據,然后進行后續的處理。(注意:兩個線程同時向一個文件寫數據可能會有同步的問題,FFmpeg內部可能沒有做多線程安全訪問的處理,所以最好在自己線程里加一個鎖進行互斥,從而保護臨界區的安全)
其中,讀取攝像頭數據的線程的處理代碼如下:
DWORD WINAPI CAVInputStream::CaptureVideoThreadFunc(LPVOID lParam)
CAVInputStream * pThis = (CAVInputStream*)lParam;
pThis->
ReadVideoPackets();
int CAVInputStream::ReadVideoPackets()
prepare before decode
and encode
dec_pkt = (AVPacket *)
av_malloc(
sizeof(AVPacket));
if ((ret =
av_read_frame(m_pVidFmtCtx, dec_pkt)) >=
0)
pframe =
av_frame_alloc();
ret =
avcodec_decode_video2(m_pVidFmtCtx->streams[dec_pkt->stream_index]->codec, pframe, &dec_got_frame, dec_pkt);
av_log(
NULL, AV_LOG_ERROR,
"Decoding failed\n");
CAutoLock lock(&m_WriteLock);
m_pVideoCBFunc(m_pVidFmtCtx->streams[dec_pkt->stream_index], m_pVidFmtCtx->streams[m_videoindex]->codec->pix_fmt, pframe,
av_gettime() - m_start_time);
ATLTRACE(
"Could not read video frame\n");
在CAVInputStream::ReadVideoPackets()函數中不停地調用 av_read_frame讀取采集到的圖像幀,接着調用avcodec_decode_video2進行“解碼”,這樣獲得了原始的圖像,圖像可能是RGB或YUV格式。解碼后的圖像通過m_pVideoCBFunc指向的回調函數回調給上層處理,回調函數里可進行后續的一些操作,比如對視頻幀編碼或直接顯示。
3.6 編碼、封裝成文件
CAVInputStream的工作線程里面讀取到的視頻幀和音頻包通過回調函數傳給CAVOuputStream類去處理。下面是兩個回調函數的實現:
LRESULT CALLBACK VideoCaptureCallback(AVStream * input_st, enum PixelFormat pix_fmt, AVFrame *pframe, INT64 lTimeStamp)
if(gpMainFrame->
IsPreview())
gpMainFrame->m_Painter.
Play(input_st, pframe);
gpMainFrame->m_OutputStream.
write_video_frame(input_st, pix_fmt, pframe, lTimeStamp);
LRESULT CALLBACK AudioCaptureCallback(AVStream * input_st, AVFrame *pframe, INT64 lTimeStamp)
gpMainFrame->m_OutputStream.
write_audio_frame(input_st, pframe, lTimeStamp);
視頻回調函數調用了CAVOutputStream的成員函數write_video_frame,這個函數對傳入的圖像幀進行編碼(H264),並且寫到指定的封裝文件;而音頻回調函數則調用了CAVOutputStream的另外一個成員函數write_audio_frame,這個函數負責對音頻編碼(AAC),然后輸出到指定的封裝文件。下面是Write_video_frame函數的實現代碼:
int CAVOutputStream::write_video_frame(AVStream * input_st, enum PixelFormat pix_fmt, AVFrame *pframe, INT64 lTimeStamp)
if(m_first_vid_time1 ==
-1)
TRACE(
"First Video timestamp: %ld \n", lTimeStamp);
m_first_vid_time1 = lTimeStamp;
AVRational time_base_q = {
1, AV_TIME_BASE };
if(img_convert_ctx ==
NULL)
img_convert_ctx =
sws_getContext(m_width, m_height,
pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC,
NULL,
NULL,
NULL);
sws_scale(img_convert_ctx, (
const
uint8_t*
const*)pframe->data, pframe->linesize,
0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
pFrameYUV->width = pframe->width;
pFrameYUV->height = pframe->height;
pFrameYUV->format = PIX_FMT_YUV420P;
av_init_packet(&enc_pkt);
ret =
avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);
if(m_first_vid_time2 ==
-1)
m_first_vid_time2 = lTimeStamp;
enc_pkt.stream_index = video_st->index;
enc_pkt.pts = (INT64)video_st->time_base.den * lTimeStamp/AV_TIME_BASE;
ret =
av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
char tmpErrString[
128] = {
0};
ATLTRACE(
"Could not write video frame, error: %s\n",
av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, ret));
av_packet_unref(&enc_pkt);
av_free_packet(&enc_pkt);
ATLTRACE(
"Buffer video frame, timestamp: %I64d.\n", lTimeStamp);
Write_video_frame和write_audio_frame是CAVOutputStream的兩個很重要的函數,其中對音頻包的處理略為復雜一些,主要是因為輸入的音頻和編碼后的音頻的frame_size不一樣,中間需要一個Fifo作緩沖隊列。另外時間戳PTS的計算也是很關鍵的,弄得不好保存的文件播放視音頻就不同步了,具體怎么實現你們看代碼吧。
工程的代碼下載地址:點擊打開鏈接
(補充說明:有些網友下載了工程反映例子在Win7,Win10系統上運行不了,這是因為枚舉設備列表的EnumDevice.dll沒有編譯成Release版造成的,可以從我的另外一個資源里下載這個庫的代碼或已編譯生成的DLL。)
from:https://blog.csdn.net/zhoubotong2012/article/details/79338093