如何用FFmpeg API采集攝像頭視頻和麥克風音頻,並實現錄制文件的功能


之前一直用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");
//printf("Device Info=============\n");
avformat_open_input(&pFmtCtx, "video=dummy", iformat, &options);
//printf("========================\n");

我用了一種最傳統的做法來解決,就是通過Directshow的COM接口來枚舉設備,工程里面的EnumDevice接口就實現了枚舉設備的功能,函數原型如下:

//枚舉指定類型的所有采集設備的名稱
ENUMDEVICE_API HRESULT EnumDevice(CAPTURE_DEVICE_TYPE type, char * deviceList[], int nListLen, int & iNumCapDevices);

當然,如果讀者用的采集設備是固定一種,那么可以固定采集設備的名稱,這樣做可以省點事。

3.2 注冊FFmpeg庫

av_register_all();
avdevice_register_all();

這兩個API可以在程序的構造函數和窗口初始化里面調用。

3.3 打開輸入設備

首先需要指定采集設備的名稱。如果是視頻設備類型,則名稱以“video=”開頭;如果是音頻設備類型,則名稱以“audio=”開頭。調用avformat_open_input接口打開設備,將設備名稱作為參數傳進去,注意這個設備名稱需要轉成UTF-8編碼。然后調用avformat_find_stream_info獲取流的信息,得到視頻流或音頻流的索引號,之后會頻繁用到這個索引號來定位視頻和音頻的Stream信息。接着,調用avcodec_open2打開視頻解碼器或音頻解碼器,實際上,我們可以把設備也看成是一般的文件源,而文件一般采用某種封裝格式,要播放出來需要進行解復用,分離成裸流,然后對單獨的視頻流、音頻流進行解碼。雖然采集出來的圖像或音頻都是未編碼的,但是按照FFmpeg的常規處理流程,我們需要加上“解碼”這個步驟。

int i;
m_pInputFormat = av_find_input_format("dshow");
ASSERT(m_pInputFormat != NULL);

if(!m_video_device.empty())
{
int res = 0;

string device_name = "video=" + m_video_device;

string device_name_utf8 = AnsiToUTF8(device_name.c_str(), device_name.length()); //轉成UTF-8,解決設備名稱包含中文字符出現亂碼的問題

//Set own video device's name
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");
return false;
}
//input video initialize
if (avformat_find_stream_info(m_pVidFmtCtx, NULL) < 0)
{
ATLTRACE("Couldn't find video stream information.(無法獲取流信息)\n");
return false;
}
m_videoindex = -1;
for (i = 0; i < m_pVidFmtCtx->nb_streams; i++)
{
if (m_pVidFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
m_videoindex = i;
break;
}
}

if (m_videoindex == -1)
{
ATLTRACE("Couldn't find a video stream.(沒有找到視頻流)\n");
return false;
}
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");
return false;
}
}

//////////////////////////////////////////////////////////

if(!m_audio_device.empty())
{
string device_name = "audio=" + m_audio_device;

string device_name_utf8 = AnsiToUTF8(device_name.c_str(), device_name.length()); //轉成UTF-8,解決設備名稱包含中文字符出現亂碼的問題

//Set own audio device's name
if (avformat_open_input(&m_pAudFmtCtx, device_name_utf8.c_str(), m_pInputFormat, &device_param) != 0){

ATLTRACE("Couldn't open input audio stream.(無法打開輸入流)\n");
return false;
}

//input audio initialize
if (avformat_find_stream_info(m_pAudFmtCtx, NULL) < 0)
{
ATLTRACE("Couldn't find audio stream information.(無法獲取流信息)\n");
return false;
}
m_audioindex = -1;
for (i = 0; i < m_pAudFmtCtx->nb_streams; i++)
{
if (m_pAudFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
{
m_audioindex = i;
break;
}
}
if (m_audioindex == -1)
{
ATLTRACE("Couldn't find a audio stream.(沒有找到音頻流)\n");
return false;
}
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");
return false;
}
}

3.4 初始化輸出流

前面我們已經初始化了InputStream,現在需要對OutputStream進行初始化,而要初始化輸出流需要知道視頻采集的分辨率,幀率,輸出像素格式等信息,還有音頻采集設備的采樣率,聲道數,Sample格式,而這些信息可通過CAVInputStream類的接口來獲取到。下面是初始化OutputStream的代碼:

m_InputStream.SetVideoCaptureCB(VideoCaptureCallback);
m_InputStream.SetAudioCaptureCB(AudioCaptureCallback);

bool bRet;
bRet = m_InputStream.OpenInputStream(); //初始化采集設備
if(!bRet)
{
MessageBox(_T("打開采集設備失敗"), _T("提示"), MB_OK|MB_ICONWARNING);
return 1;
}

int cx, cy, fps;
AVPixelFormat pixel_fmt;
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); //設置音頻編碼器屬性
}

//從Config.INI文件中讀取錄制文件路徑
P_GetProfileString(_T("Client"), "file_path", m_szFilePath, sizeof(m_szFilePath));

bRet = m_OutputStream.OpenOutputStream(m_szFilePath); //設置輸出路徑
if(!bRet)
{
MessageBox(_T("初始化輸出失敗"), _T("提示"), MB_OK|MB_ICONWARNING);
return 1;
}

在上面的代碼片段中,首先設置了視頻和音頻的數據回調函數。當采集開始時,視頻和音頻數據就會傳遞給相應的函數去處理,在該程序中,回調函數主要對圖像或音頻進行編碼,然后封裝成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");
return false;
}

m_start_time = av_gettime();

m_exit_thread = false;

if(!m_video_device.empty())
{
m_hCapVideoThread = CreateThread(
NULL, // default security attributes
0, // use default stack size
CaptureVideoThreadFunc, // thread function name
this, // argument to thread function
0, // use default creation flags
NULL); // returns the thread identifier
}

if(!m_audio_device.empty())
{
m_hCapAudioThread = CreateThread(
NULL, // default security attributes
0, // use default stack size
CaptureAudioThreadFunc, // thread function name
this, // argument to thread function
0, // use default creation flags
NULL); // returns the thread identifier
}

return true;
}

StartCapture函數分別建立了一個讀取視頻包和讀取音頻包的線程,兩個線程各自獨立工作,分別從視頻采集設備,音頻采集設備獲取到數據,然后進行后續的處理。(注意:兩個線程同時向一個文件寫數據可能會有同步的問題,FFmpeg內部可能沒有做多線程安全訪問的處理,所以最好在自己線程里加一個鎖進行互斥,從而保護臨界區的安全)

其中,讀取攝像頭數據的線程的處理代碼如下:

DWORD WINAPI CAVInputStream::CaptureVideoThreadFunc(LPVOID lParam)
{
CAVInputStream * pThis = (CAVInputStream*)lParam;

pThis->ReadVideoPackets();

return 0;
}

int CAVInputStream::ReadVideoPackets()
{
if(dec_pkt == NULL)
{
////prepare before decode and encode
dec_pkt = (AVPacket *)av_malloc(sizeof(AVPacket));
}

int encode_video = 1;
int ret;

//start decode and encode

while (encode_video)
{
if (m_exit_thread)
break;

AVFrame * pframe = NULL;
if ((ret = av_read_frame(m_pVidFmtCtx, dec_pkt)) >= 0)
{
pframe = av_frame_alloc();
if (!pframe)
{
ret = AVERROR(ENOMEM);
return ret;
}
int dec_got_frame = 0;
ret = avcodec_decode_video2(m_pVidFmtCtx->streams[dec_pkt->stream_index]->codec, pframe, &dec_got_frame, dec_pkt);
if (ret < 0)
{
av_frame_free(&pframe);
av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
break;
}
if (dec_got_frame)
{
if(m_pVideoCBFunc)
{
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);
}

av_frame_free(&pframe);
}
else
{
av_frame_free(&pframe);
}

av_free_packet(dec_pkt);
}
else
{
if (ret == AVERROR_EOF)
encode_video = 0;
else
{
ATLTRACE("Could not read video frame\n");
break;
}
}
}

return 0;
}

在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);
return 0;
}

//采集到的音頻數據回調
LRESULT CALLBACK AudioCaptureCallback(AVStream * input_st, AVFrame *pframe, INT64 lTimeStamp)
{
gpMainFrame->m_OutputStream.write_audio_frame(input_st, pframe, lTimeStamp);
return 0;
}

視頻回調函數調用了CAVOutputStream的成員函數write_video_frame,這個函數對傳入的圖像幀進行編碼(H264),並且寫到指定的封裝文件;而音頻回調函數則調用了CAVOutputStream的另外一個成員函數write_audio_frame,這個函數負責對音頻編碼(AAC),然后輸出到指定的封裝文件。下面是Write_video_frame函數的實現代碼:

//input_st -- 輸入流的信息
//input_frame -- 輸入視頻幀的信息
//lTimeStamp -- 時間戳,時間單位為/1000000
//
int CAVOutputStream::write_video_frame(AVStream * input_st, enum PixelFormat pix_fmt, AVFrame *pframe, INT64 lTimeStamp)
{
if(video_st == NULL)
return -1;

//ATLTRACE("Video timestamp: %ld \n", 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)
{
//camera data may has a pix fmt of RGB or sth else,convert it to YUV420
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;

enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);

int ret;
int enc_got_frame = 0;
ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);

if (enc_got_frame == 1)
{
//printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, enc_pkt.size);

if(m_first_vid_time2 == -1)
{
m_first_vid_time2 = lTimeStamp;
}

enc_pkt.stream_index = video_st->index;

//enc_pkt.pts= av_rescale_q(lTimeStamp, time_base_q, video_st->time_base);
enc_pkt.pts = (INT64)video_st->time_base.den * lTimeStamp/AV_TIME_BASE;

m_vid_framecnt++;

ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
if(ret < 0)
{
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);
return ret;
}

av_free_packet(&enc_pkt);
}
else if(ret == 0)
{
ATLTRACE("Buffer video frame, timestamp: %I64d.\n", lTimeStamp); //編碼器緩沖幀
}

return 0;
}

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


免責聲明!

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



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