录制过程中音视频帧数据的转换
==============

#### 问题现象

通过OH\_AVScreenCapture获取原始码流的方式录制mp4文件，在录制过程中，需要将音视频帧转成图片或PCM数据，进行图像识别或语音识别，如何在录制过程中将音视频帧数据转换为JPEG图片或PCM音频。

#### 背景知识

录屏取码流相关可详细参考[AVScreenCapture录屏取码流](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-avscreencapture-for-buffer)。通过录屏获取码流的方式获取的流数据可根据场景，进行不同的处理。例如：

*   对接NativeImage作为消费者端，提供Surface关联OpenGL外部纹理，具体使用请参考[NativeImage开发指导](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/native-image-guidelines)。
*   对接编码器、封装器进行数据的编码、封装，具体使用请参考[视频编码](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/video-encoding)、[媒体数据封装](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/audio-video-muxer)。
*   对接网络模块，上传流数据到服务器，具体使用请参考[实现同步读写流](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/remote-communication-syncstreamreq)。

#### 解决方案

要在录制过程中获取音视频帧数据转换为对应格式的图片和音频，可以在[AVScreenCapture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/capi-avscreencapture)的OH\_AVBuffer中获取buffer流数据保存，再将保存的数据进行转换。

1.  从buffer流中获取数据，获取数据为PCM格式码流。
    
    void Buffer2Png(OH\_AVBuffer\* buffer)
    {
        int bufferLen = OH\_AVBuffer\_GetCapacity(buffer);
        OH\_LOG\_INFO (LOG\_APP,"png length: %{public}d",bufferLen);
        OH\_NativeBuffer \*nativeBuffer = OH\_AVBuffer\_GetNativeBuffer(buffer);
        OH\_NativeBuffer\_Config config;
        OH\_NativeBuffer\_GetConfig(nativeBuffer, &config);
        int32\_t videoSize = config.height \* config.width \* 4;
        uint8\_t \*buf = OH\_AVBuffer\_GetAddr(buffer);
        TestSavePng(config.width, config.height, (uint8\_t \*)buf);
    }
    
    void OnBufferAvailable(OH\_AVScreenCapture \*capture, OH\_AVBuffer \*buffer, OH\_AVScreenCaptureBufferType bufferType, int64\_t timestamp, void \*userData) 
    {
       _ // 处于录屏取码流状态。_
        if (g\_isCaptureStreamRunning) {
          _  // 视频buffer。_
            Buffer2Png(buffer);
            OH\_LOG\_INFO(LOG\_APP, "this is an info level log");
        }
    }
    
    struct OH\_AVScreenCapture \* g\_capture;
    _// 开始录屏时调用StartScreenCapture。_
    static napi\_value StartScreenCapture(napi\_env env, napi\_callback\_info info) 
    {
        size\_t argc = 1;
        napi\_value args\[1\] = {nullptr};
     _   // 获取参数。_
        napi\_get\_cb\_info(env, info, &argc, args, nullptr, nullptr);
        int32\_t Frame\_Rate;
        napi\_get\_value\_int32(env, args\[0\], &Frame\_Rate);
        g\_isCaptureStreamRunning = true;
       _ // 实例化ScreenCapture。_
        g\_capture = OH\_AVScreenCapture\_Create();
      _  // 设置回调 。_
        OH\_AVScreenCapture\_SetErrorCallback(g\_capture, OnError, nullptr);
        OH\_AVScreenCapture\_SetStateCallback(g\_capture, OnStateChange, nullptr);
        OH\_AVScreenCapture\_SetDataCallback(g\_capture, OnBufferAvailable, nullptr);
        OH\_VideoCaptureInfo videocapinfo = {.videoFrameWidth = 768, .videoFrameHeight = 1280, .videoSource = OH\_VIDEO\_SOURCE\_SURFACE\_RGBA};
        OH\_VideoInfo videoinfo = {.videoCapInfo = videocapinfo};
        OH\_AVScreenCaptureConfig config = {.captureMode = OH\_CAPTURE\_HOME\_SCREEN,.dataType = OH\_ORIGINAL\_STREAM,.videoInfo = videoinfo};
        OH\_AVScreenCapture\_Init(g\_capture, config);
      _  // 开始录屏。_
        OH\_AVScreenCapture\_StartScreenCapture(g\_capture);
       _ // mic开关设置。_
        OH\_AVScreenCapture\_SetMicrophoneEnabled(g\_capture, false);
     _   // 可选 设置录屏时的最大帧率 需在启动后调用。_
        OH\_AVScreenCapture\_SetMaxVideoFrameRate(g\_capture, Frame\_Rate);
     _   // 返回调用结果，仅返回随意值。_
        napi\_value sum;
        napi\_create\_double(env, 0, &sum);
        return sum;
    }
    
2.  要将帧数据进行格式转换，可以将音视频buffer提取到的PCM格式数据，使用自定义PNG图片编码方法写进图片文件中。
    
    PNG图片由多个文件块构成，每个块都有特定的用途，常见PNG文件块组成如下表所示。
    
    | 
    块名
    
     | 
    
    是否必须
    
     | 
    
    说明
    
     |
    | :-- | :-- | :-- |
    | 
    
    IHDR
    
     | 
    
    是
    
     | 
    
    图像头信息（宽度、高度、比特深度、颜色类型等）。
    
     |
    | 
    
    PLTE
    
     | 
    
    否
    
     | 
    
    调色板（仅当颜色类型为索引颜色时使用）。
    
     |
    | 
    
    tRNS
    
     | 
    
    否
    
     | 
    
    透明度信息（仅当颜色类型为索引或 RGB 时使用）。
    
     |
    | 
    
    IDAT
    
     | 
    
    是
    
     | 
    
    图像数据（经过压缩的图像像素数据）。
    
     |
    | 
    
    IEND
    
     | 
    
    是
    
     | 
    
    图像结束标识。
    
     |
    
    一个简单的PNG文件结构必须包含图像基本信息及图像数据。综上所述，可以参考以下步骤将录制传入的PCM格式buffer手动转化为PNG图片：
    
    1.  初始化定义CRC32冗余校验码，用于计算PNG文件中各块的校验和，验证数据完整性。
    2.  宏定义编码预处理函数，用于后续将buffer数据写入PNG各个文件块中。
    3.  将数据信息分别写入IHDR（图像头信息块），IDAT（图像数据块），IEND（图像结束标识）。
    
    完整示例参考如下：
    
    void  Svpng (FILE\* out, unsigned w, unsigned h, const  unsigned  char \* img, int alpha);
    SVPNG\_LINKAGE void Svpng(SVPNG\_OUTPUT, unsigned w, unsigned h, const unsigned char\* img, int alpha) 
    {
        static const unsigned T\[\] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, 
        _/\* CRC32校验码表 \*/_    0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c };
        unsigned a = 1, b = 0, c, p = w \* (alpha ? 4 : 3) + 1, x, y, i;   _/\* 初始化CRC校验码的变量 \*/_
    #define SVPNG\_U8A(ua, l) do { \\
        for (i = 0; i < (l); i++) { \\
            SVPNG\_PUT((ua)\[i\]); \\
        } \\
    } while(0)
    #define SVPNG\_U32(u) do { \\
        SVPNG\_PUT(((u) >> 24) & 255); \\
        SVPNG\_PUT(((u) >> 16) & 255); \\
        SVPNG\_PUT(((u) >> 8) & 255); \\
        SVPNG\_PUT((u) & 255); \\
    } while(0)
    #define SVPNG\_U8C(u) do { \\
        SVPNG\_PUT(u); \\
        c ^= (u); \\
        c = (c >> 4) ^ T\[c & 15\]; \\
        c = (c >> 4) ^ T\[c & 15\]; \\
    } while(0)
    #define SVPNG\_U8AC(ua, l) do { \\
        for (i = 0; i < (l); i++) { \\
            SVPNG\_U8C((ua)\[i\]); \\
        } \\
    } while(0)
    #define SVPNG\_U16LC(u) do { SVPNG\_U8C((u) & 255); SVPNG\_U8C(((u) >> 8) & 255); } while(0)
    #define SVPNG\_U32C(u) do { SVPNG\_U8C((u) >> 24); SVPNG\_U8C(((u) >> 16) & 255); SVPNG\_U8C(((u) >> 8) & 255); SVPNG\_U8C((u) & 255); } while(0)
    #define SVPNG\_U8ADLER(u) do { SVPNG\_U8C(u); a = (a + (u)) % 65521; b = (b + a) % 65521; } while(0)
    #define SVPNG\_BEGIN(s, l) do { SVPNG\_U32(l); c = ~0U; SVPNG\_U8AC(s, 4); } while(0)
    #define SVPNG\_END() SVPNG\_U32(~c)
        SVPNG\_U8A("\\x89PNG\\r\\n\\32\\n", 8);         _  /\* 写入PNG文件头 \*/_
        SVPNG\_BEGIN("IHDR", 13);                  _  /\* 将图像头信息写入IHDR块（图像头信息）中 \*/_
        SVPNG\_U32C(w); SVPNG\_U32C(h);              _ /\* 图像宽高(8字节) \*/_
        SVPNG\_U8C(8); SVPNG\_U8C(alpha ? 6 : 2);   _  /\* 深度为 8，颜色类型为带透明度（RGBA）或不带透明度（RGB） \*/_
        SVPNG\_U8AC("\\0\\0\\0", 3);                 _   /\* 压缩方法、滤波器和交错类型固定为默认值 \*/_
        SVPNG\_END();                             _   /\* 结束图像文件头写入 \*/_
        SVPNG\_BEGIN("IDAT", 2 + h \* (5 + p) + 4);  _ /\* 将图像数据写入IDAT块中 \*/_
        SVPNG\_U8AC("\\x78\\1", 2);                  _  /\* 写入压缩块起始头（2字节） \*/_
        for (y = 0; y < h; y++) {                 _  /\* 每行作为一个压缩块（Default Block） \*/_
            SVPNG\_U8C(y == h - 1);                 _ /\* 写入一个字节判断是否到达最后一行，若是最后一行写1，否则写0 \*/_
            SVPNG\_U16LC(p); SVPNG\_U16LC(~p);       _ /\* 写入两个16位整数（共4字节），分别表示块大小及其1的补码，用于校验完整性 \*/_
            SVPNG\_U8ADLER(0);                     _  /\* 写入一个字节表示未使用滤波 \*/_
            for (x = 0; x < p - 1; x++, img++)
                SVPNG\_U8ADLER(\*img);             _   /\* 将图像的每个像素依次写入压缩块中，完成图片数据压缩 \*/_
        }
        SVPNG\_U32C((b << 16) | a);                 _ /\* 写入4字节整数用于表示压缩块的结束和Adler-32校验和 \*/_
        SVPNG\_END();                             _   /\* } \*/_
        SVPNG\_BEGIN("IEND", 0); SVPNG\_END();    _    /\* 写入图像结束块标识IEND \*/_
    }
    #endif _/\* SVPNG\_INC\_ \*/_
    
3.  保存转换后的PNG图片，并在ArkTS侧通过保存文件路径展示图片。
    
    void TestSavePng(unsigned int w, unsigned int h, const unsigned char \*img)
    {
        static int num = 0;
        std::string tmpDir = std::getenv("TMPDIR"); _ // /data/storage/el2/base/cache_
        std::string filename = tmpDir + "/" + GETPNGNAME + std::to\_string(num) + ".png";
        FILE \*fp = fopen(filename.c\_str(), "wb");
        Svpng(fp, w, h, img, 1);
        fclose(fp);
        fp = nullptr;
    }
    

#### 总结

将录制获取的音视频帧数据转换为可展示的PNG图片，可按照以下步骤。

1.  创建录制音视频流，获取音视频流数据buffer（PCM格式）。
2.  将获取的buffer传入自定义PNG编码函数中。
3.  保存PNG图片并在ArkTS侧展示。