1. 程式人生 > >新手學習FFmpeg - 呼叫API完成錄屏

新手學習FFmpeg - 呼叫API完成錄屏

呼叫FFMPEG Device API完成Mac錄屏功能。

呼叫FFMPEG提供的API來完成錄屏功能,大致的思路是:

  1. 開啟輸入裝置.
  2. 開啟輸出裝置.
  3. 從輸入裝置讀取視訊流,然後經過解碼->編碼,寫入到輸出裝置.
+--------------------------------------------------------------+
|   +---------+    decode               +------------+         |
|   | Input   | ----------read -------->|  Output    |         |
|   +---------+                 encode  +------------+         |
+--------------------------------------------------------------+

因此主要使用的API就是:

  1. avformat_open_input
  2. avcodec_find_decoder
  3. av_read_frame
  4. avcodec_send_packet/avcodec_receive_frame
  5. 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。

  1. 獲取指定格式的輸入裝置
    pAVInputFormat = av_find_input_format("avfoundation");

通過指定格式名稱獲取到AVInputFormat結構體。

  1. 開啟裝置
    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容器規範的幀)既可。 獲取編碼器大致分為兩個步驟:

  1. 構建編碼器上下文(AVFormatContext)
  2. 匹配合適的編碼器(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);
    }
  • 編解碼

從輸入裝置讀取的是原生的資料流,也就是經過裝置編碼之後的資料。 需要先將原生資料進行解碼,變成程式可讀的資料,在編碼成輸出裝置可識別的資料。 所以這一步的流程是:

  1. 解碼輸入裝置資料
  2. 轉碼
  3. 編碼寫入輸出裝置

通過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 decoding\n");
                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 decoding\n");
                    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 encoding\n");
                    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 encoding\n");
                        exit(1);
                    }

                    ... do something;

                    av_packet_unref(&outPacket);
                }

以後就按照這種的處理邏輯,不停的從輸入裝置讀取資料,然後經過解碼->轉碼->編碼,最後傳送到輸出裝置。 這樣就完成了錄屏功能。
上面是大致處理思路,完整原始碼可以參考 (https://github.com/andy-zhangtao/ffmpeg-examples/tree/master/ScreenRecord