新手學習FFmpeg – 調用API完成錄屏
- 2019 年 10 月 3 日
- 筆記
調用FFMPEG Device API完成Mac錄屏功能。
調用FFMPEG提供的API來完成錄屏功能,大致的思路是:
- 打開輸入設備.
- 打開輸出設備.
- 從輸入設備讀取影片流,然後經過解碼->編碼,寫入到輸出設備.
+--------------------------------------------------------------+ | +---------+ decode +------------+ | | | Input | ----------read -------->| Output | | | +---------+ encode +------------+ | +--------------------------------------------------------------+
因此主要使用的API就是:
- avformat_open_input
- avcodec_find_decoder
- av_read_frame
- avcodec_send_packet/avcodec_receive_frame
- avcodec_send_frame/avcodec_receive_packet
- 打開輸入設備
如果使用FFmpeg提供的-list_devices
命令可以查詢到當前支援的設備,其中分為兩類:
- AVFoundation video devices
- AVFoundation audio devices
AVFoundation 是Mac特有的基於時間的多媒體處理框架。本次是演示錄屏功能,因此忽略掉audio設備,只考慮video設備。在avfoundation.m
文件中沒有發現可以程式化讀取設備的API。FFmpeg官方也說明沒有程式化讀取設備的方式,通用方案是解析日誌來獲取設備(https://trac.ffmpeg.org/wiki/DirectShow#Howtoprogrammaticallyenumeratedevices),下一篇再研究如何通過日誌獲取當前支援的設備,本次就直接寫死設備ID。
- 獲取指定格式的輸入設備
pAVInputFormat = av_find_input_format("avfoundation");
通過指定格式名稱獲取到AVInputFormat結構體。
- 打開設備
value = avformat_open_input(&pAVFormatContext, "1", pAVInputFormat, &options); if (value != 0) { cout << "nerror in opening input device"; exit(1); }
"1"指代的是設備ID。 options是打開設備時輸入參數,
// 記錄滑鼠 value = av_dict_set(&options, "capture_cursor", "1", 0); if (value < 0) { cout << "nerror in setting capture_cursor values"; exit(1); } // 記錄滑鼠點擊事件 value = av_dict_set(&options, "capture_mouse_clicks", "1", 0); if (value < 0) { cout << "nerror in setting capture_mouse_clicks values"; exit(1); } // 指定像素格式 value = av_dict_set(&options, "pixel_format", "yuyv422", 0); if (value < 0) { cout << "nerror in setting pixel_format values"; exit(1); }
通過value值判斷設備是否正確打開。 然後獲取設備影片流ID(解碼數據包時需要判斷是否一致),再獲取輸入編碼器(解碼時需要)。
- 打開輸出設備
假設需要將從輸入設備讀取的數據保存成mp4
格式的文件。
將影片流保存到文件中,只需要一個合適的編碼器(用於生成符合MP4容器規範的幀)既可。 獲取編碼器大致分為兩個步驟:
- 構建編碼器上下文(AVFormatContext)
- 匹配合適的編碼器(AVCodec)
構建編碼器:
// 根據output_file後綴名推測合適的編碼器 avformat_alloc_output_context2(&outAVFormatContext, NULL, NULL, output_file); if (!outAVFormatContext) { cout << "nerror in allocating av format output context"; exit(1); }
匹配編碼器:
output_format = av_guess_format(NULL, output_file, NULL); if (!output_format) { cout << "nerror in guessing the video format. try with correct format"; exit(1); } video_st = avformat_new_stream(outAVFormatContext, NULL); if (!video_st) { cout << "nerror in creating a av format new stream"; exit(1); }
- 編解碼
從輸入設備讀取的是原生的數據流,也就是經過設備編碼之後的數據。 需要先將原生數據進行解碼,變成程式可讀
的數據,在編碼成輸出設備可識別的數據。 所以這一步的流程是:
- 解碼輸入設備數據
- 轉碼
- 編碼寫入輸出設備
通過av_read_frame
從輸入設備讀取數據:
while (av_read_frame(pAVFormatContext, pAVPacket) >= 0) { ... }
對讀取後的數據進行拆包,找到我們所感興趣的數據
// 最開始沒有做這種判斷,出現不可預期的錯誤。 在官網example中找到這句判斷,但還不是很清楚其意義。應該和packet封裝格式有關 pAVPacket->stream_index == VideoStreamIndx
從FFmpeg 4.1開始,有了新的編解碼函數。 為了長遠考慮,直接使用新API。 使用avcodec_send_packet
將輸入設備的數據發往解碼器
進行解碼,然後使用avcodec_receive_frame
從解碼器
接受解碼之後的數據幀。程式碼大概是下面的樣子:
value = avcodec_send_packet(pAVCodecContext, pAVPacket); if (value < 0) { fprintf(stderr, "Error sending a packet for decodingn"); exit(1); } while(1){ value = avcodec_receive_frame(pAVCodecContext, pAVFrame); if (value == AVERROR(EAGAIN) || value == AVERROR_EOF) { break; } else if (value < 0) { fprintf(stderr, "Error during decodingn"); exit(1); } .... do something }
讀取到數據幀後,就可以對每一幀進行轉碼
:
sws_scale(swsCtx_, pAVFrame->data, pAVFrame->linesize, 0, pAVCodecContext->height, outFrame->data,outFrame->linesize);
最後將轉碼後的幀封裝成輸出設備可設別的數據包格式。也就是解碼的逆動作,使用avcodec_send_frame
將每幀發往編碼器進行編碼,通過avcodec_receive_packet
一直接受編碼之後的數據包。處理邏輯大致是:
value = avcodec_send_frame(outAVCodecContext, outFrame); if (value < 0) { fprintf(stderr, "Error sending a frame for encodingn"); exit(1); } while (value >= 0) { value = avcodec_receive_packet(outAVCodecContext, &outPacket); if (value == AVERROR(EAGAIN) || value == AVERROR_EOF) { break; } else if (value < 0) { fprintf(stderr, "Error during encodingn"); exit(1); } ... do something; av_packet_unref(&outPacket); }
以後就按照這種的處理邏輯,不停的從輸入設備讀取數據,然後經過解碼->轉碼->編碼,最後發送到輸出設備。 這樣就完成了錄屏功能。
上面是大致處理思路,完整源程式碼可以參考 (https://github.com/andy-zhangtao/ffmpeg-examples/tree/master/ScreenRecord) .