了解過ffmpeg的人都知道,利用ffmpeg命令即可實現將電腦中攝像頭的畫面發布出去,例如發布為UDP,RTP,RTMP等,甚至可以發布為HLS,將m3u8文件和視頻ts片段保存至Web服務器,普通的播放器就可以直接播放他們。
的確,但是,但是作為一個技術愛好者,相信大家都是對里面的機制與原理是感興趣的,我們希望通過我們寫代碼來實現它。另外,ffmpeg命令發布出來的攝像頭畫面我們似乎要加一些水印、顯示一些自定義的文字,可能就不是那么靈活了。舉個例子來說,我們想畫面中主要畫面是電腦桌面,左上角顯示攝像頭的畫面,相信現有的ffmpeg命令實現不了了吧!那有什么辦法嗎?答案是肯定的,假如我們找到ffmpeg中采集視頻的部分,把采集到的畫面替換成我們需要的不就行了嗎?即我們抓一下桌面畫面,再抓一下攝像頭畫面,然后通過一定的縮放疊加在一塊就行了。
講到這里,我們就有一個目的了,想想剛才我們的設想,我們可以想到ffmpeg中的一個功能,將電腦桌面的畫面保存到文件中(當然也可以發布到網絡中),這個功能是不是和我們的需求特別相像,通過百度,我們可以得到以下一段代碼:
- AVFormatContext *pFormatCtx = avformat_alloc_context();
- AVInputFormat *ifmt=av_find_input_format("gdigrap");
- avformat_open_input(&pFormatCtx, 0, ifmt,NULL);
通過以上代碼我們可以打開一個設備gdigrap(錄制windows桌面),打開后,我們就可以從中一幀一幀地讀出畫面了,我們在libavdevice目錄中可以找到一個叫gdigrap.c的文件,里面實現了ffmpeg一個設備的基本實現,結構如下:
- /** gdi grabber device demuxer declaration */
- AVInputFormat ff_gdigrab_demuxer = {
- .name = "gdigrab",
- .long_name = NULL_IF_CONFIG_SMALL("GDI API Windows frame grabber"),
- .priv_data_size = sizeof(struct gdigrab),
- .read_header = <span style="color:#ff6666;">gdigrab_read_header</span>,
- .read_packet = <span style="color:#ff6666;">gdigrab_read_packet</span>,
- .read_close = <span style="color:#ff6666;">gdigrab_read_close</span>,
- .flags = AVFMT_NOFILE,
- .priv_class = &gdigrab_class,
- };
注意紅色部分的三個方法,就是該設備的開啟、讀數據、關閉,簡單地掃描一下三個方法的代碼,里面的代碼還是比較簡單,就是一些DC的操作(熟悉windows窗口繪圖相關知識的應該了解),讀到這,我們就有思路了,我們可以模仿這個文件來實現我們想要的,考慮到這個文件是編譯到ffmpeg庫的,所以我們希望不破壞ffmpeg的框架的情況下,增加幾個接口(或者說回調),當打開設備時,回調一下我們的接口,當讀畫面的時候,回調一下我們的接口,當關閉設備時,也回調一下我們的接口,這樣實際上我們新寫的這個C文件只是一個架子,里面的具體實現交給外部使用者,所以我們定義以下三個接口:
- typedef int (*fnVideoCapInitCallback)(int index, int width, int height, int framerate);
- typedef int (*fnVideoCapReadCallback)(int index, unsigned char *buff, int len, int width, int height, int framerate, int format);
- typedef int (*fnVideoCapCloseCallback)(int index);
- void av_setVideoCapInitCallback(fnVideoCapInitCallback callback);
- void av_setVideoCapReadCallback(fnVideoCapReadCallback callback);
- void av_setVideoCapCloseCallback(fnVideoCapCloseCallback callback);
下面是實現結構體中的三個方法的具體實現:
- static int mygrab_read_header(AVFormatContext *s1)
- {
- struct mygrab *mygrab = s1->priv_data;
- AVStream *st = NULL;
- int ret = 0;
- printf("call mygrab_read_header\n");
- if(mygrab->width <= 0 || mygrab->height <= 0){
- av_log(s1, AV_LOG_ERROR, "video size (%d %d) is invalid\n", mygrab->width, mygrab->height);
- return -1;
- }
- st = avformat_new_stream(s1, NULL);
- if (!st) {
- ret = AVERROR(ENOMEM);
- return -1;
- }
- printf("avpriv_set_pts_info\n");
- avpriv_set_pts_info(st, 64, 1, 1000000); /* 64 bits pts in us */
- if(mygrab->framerate.num <= 0 || mygrab->framerate.den <= 0 ){
- av_log(s1, AV_LOG_WARNING, "not set framerate set default framerate\n");
- mygrab->framerate.num = 10;
- mygrab->framerate.den = 1;
- }
- mygrab->time_base = av_inv_q(mygrab->framerate);
- mygrab->time_frame = av_gettime() / av_q2d(mygrab->time_base);
- mygrab->frame_size = mygrab->width * mygrab->height * 3/2;
- st->codec->codec_type = AVMEDIA_TYPE_VIDEO;
- st->codec->codec_id = AV_CODEC_ID_RAWVIDEO;
- st->codec->pix_fmt = AV_PIX_FMT_YUV420P;//AV_PIX_FMT_RGB24;
- st->codec->width = mygrab->width;
- st->codec->height = mygrab->height;
- st->codec->time_base = mygrab->time_base;
- st->codec->bit_rate = mygrab->frame_size * 1/av_q2d(st->codec->time_base) * 8;
- <span style="color:#ff6666;"> if(s_videoCapInitCallback != NULL){
- av_log(s1, AV_LOG_INFO, "video size (%d %d) frameRate:%d\n", st->codec->width, st->codec->height, mygrab->framerate.num/mygrab->framerate.den);
- s_videoCapInitCallback(0, st->codec->width, st->codec->height, mygrab->framerate.num/mygrab->framerate.den);
- return 0;
- }</span>
- av_log(s1, AV_LOG_ERROR, "video cap not call av_setVideoCapInitCallback\n");
- return -1;
- }
- static int mygrab_read_packet(AVFormatContext *s1, AVPacket *pkt)
- {
- struct mygrab *s = s1->priv_data;
- int64_t curtime, delay;
- /* Calculate the time of the next frame */
- s->time_frame += INT64_C(1000000);
- /* wait based on the frame rate */
- for(;;) {
- curtime = av_gettime();
- delay = s->time_frame * s->time_base.num / s->time_base.den - curtime;
- if (delay <= 0) {
- if (delay < INT64_C(-1000000) * s->time_base.num / s->time_base.den) {
- /* printf("grabbing is %d frames late (dropping)\n", (int) -(delay / 16666)); */
- s->time_frame += INT64_C(1000000);
- }
- break;
- }
- av_usleep(delay);
- }
- if (av_new_packet(pkt, s->frame_size) < 0) return AVERROR(EIO);
- pkt->pts = curtime;
- <span style="color:#ff6666;"> if(s_videoCapReadCallback != NULL){
- s_videoCapReadCallback(0, pkt->data, pkt->size, s->width, s->height, s->framerate.num/s->framerate.den, AV_PIX_FMT_YUV420P);
- return pkt->size;
- }</span>
- av_log(s1, AV_LOG_ERROR, "video cap not call av_setVideoCapReadCallback\n");
- return 0;
- }
- static int mygrab_read_close(AVFormatContext *s1)
- {
- //struct mygrab *s = s1->priv_data;
- <span style="color:#ff6666;"> if(s_videoCapCloseCallback != NULL){
- s_videoCapCloseCallback(0);
- }</span>
- return 0;
- }
然后我們在alldevices.c里注冊我們的自定義設備,后面我們使用設備時就可以通過名字調用到我們剛才新建mygrab.c的邏輯了,即在
- void avdevice_register_all(void)
- {
- ......
- REGISTER_INDEV (MYGRAB, mygrab);
- ......
- }
同理,新建myoss.c實現聲音的自定義處理采集,這樣,我們就新增加了六個接口了,為了保持接口的簡潔性,我們將六個接口合並為一個接口,即
- void av_setVideoAudioCapCallbacks(fnVideoCapInitCallback callback1,fnVideoCapReadCallback callback2,fnVideoCapCloseCallback callback3
- ,fnAudioCapInitCallback callback4,fnAudioCapReadCallback callback5,fnAudioCapCloseCallback callback6);
重新編譯ffmpeg就支持我們的自定義視頻采集、音頻采集功能了,非常的靈活,比如我們要使用這些功能,一般就會寫下下面幾行代碼:
- av_setVideoAudioCapCallbacks(..,..,..,..,..,..,..); //將回調注冊進去
- AVFormatContext *pFormatCtx = avformat_alloc_context();
- AVInputFormat *ifmt=av_find_input_format("mygrap");
- avformat_open_input(&pFormatCtx, 0, ifmt,NULL); //打開自定義視頻設備
- AVFormatContext *pFormatCtx = avformat_alloc_context();
- AVInputFormat *ifmt=av_find_input_format("myoss");
- avformat_open_input(&pFormatCtx, 0, ifmt,NULL);//打開自定義音頻設備
然后按照操作其它設備一樣的操作方法,第一步中設置的回調就會依次去調用了。通過上面的折騰,我們為后面的視頻的采集音頻的采集打好了基礎,並且使用時不需要再進行麻煩的編譯,專注於我們的回調的實現就好。
ffmpeg庫編好了,就要開始實現我們最主要的攝像頭采集編碼推流了,我們注意到ffmpeg中有一個比較好的參考例子,叫做muxing.c,這個文件實現了將畫面和聲音保存為一個視頻的功能,因此我們可以在這個文件的基礎上實現我們的功能,muxing.c里的畫面和聲音是通過代碼來生成的,換成從攝像頭和聲卡采集就行了,當然我們肯定不是在里面去簡單地加代碼,要不然上面這么大的折騰就沒有意義了,我們要用上上面兩個自定義的設備,然后從設備里讀數據。
做好了以上的幾個步驟,相信就可以把攝像頭的視頻和聲音保存為mp4等文件了。
這已經離發布到網絡上很近了,因此要使視頻不保存在文件中,而是發布到網絡上,只要改幾行代碼就可以了,例如以下是發布為HLS,RTMP,FILE三個方法:
- if(type == TYPE_HLS){
- sprintf(filename, "%s\\playlist.m3u8", szPath);
- avformat_alloc_output_context2(&oc, NULL, "hls", filename);
- }else if(type == TYPE_RTMP){
- sprintf(filename, "%s", szPath);
- avformat_alloc_output_context2(&oc, NULL, "flv", filename);
- }else if(type == TYPE_FILE){
- sprintf(filename, "%s", szPath);
- avformat_alloc_output_context2(&oc, NULL, NULL, filename);
- }
這樣,我們就可以把我們的直播發布到網絡中了,這里,我們再搭建一個流媒體服務器,將流推向這個服務器,比如red5,下載一個vlc或者ffplay就可以用來播放它了,這樣一個完整的直播系統就編碼實現好了,並且直播中的畫面是任由我們發揮的,不光是攝像頭畫面,看想象力了。
以下是用MFC作出的一個攝像頭采集直播Demo,電腦瀏覽器通過web測試頁,手機PAD通過VLC等播放器就可以實時地查看,值得一提的是,web測試頁中的延時比較小,如果用播放器來播放延時比較大,這是因為web測試頁中的flash將rtmp的緩存設置成最小,而第三方播放器播放有一個緩存,而且相對比較大。
另外,Demo中攝像頭采集是用的opencv,這是由於opencv對圖像這一塊處理比較方便,可以很方便地在圖像中增加文字,反轉,變換等。
以上所述的所有源碼,可以在我的個人網站http://www.creater.net.cn/softwaredesign.html中獲取
from:https://blog.csdn.net/ce6581281/article/details/62898445