鸿蒙 next 实现摄像头视频预览&编码(一)

鸿蒙 next 即将发布,让我们先喊3遍 遥遥领先~ 遥遥领先~ 遥遥领先~

作为一门新的系统,本人也是刚入门学习中,如果对于一些理解有问题的,欢迎即使指出哈

首先这里要讲一下,在鸿蒙 next 中,要实现摄像头预览&编码有两种方式。第一种,通过摄像头的预览流&录制流来实现,其中预览很简单,直接使用 xcomponent 即可,对于编码,则可以通过创建编码器获取到的 surfaceid 传递给录制流即可。第二种是通过 nativeimage 类似于 android 的 surfacetexture 然后将纹理通过 opengl 绘制到预览 surface 和编码 surface 上去,这边文章主要将第一种简单的方式,步骤大致如下:

第一步,创建 xcomponaent,代码如下:

 XComponent({
          id: '',
          type: XComponentType.SURFACE,
          libraryname: '',
          controller: this.XcomponentController
        })
          .onLoad(() => {
            this.XcomponentController.setXComponentSurfaceSize({
              surfaceWidth: this.cameraWidth, surfaceHeight: this.cameraHeight
            })
            this.XcomponentSurfaceId = this.XcomponentController.getXComponentSurfaceId()
          })

创建 xcomponeant 的关键是获取 surfaceid,这个后面会用来传给摄像头预览流用的。

第二步,获取编码器的 surfaceid,由于目前鸿蒙没有为编码器这块提供 arkts 接口,所以需要用到 napi 作为中间桥接,通过 arkts 来调用 c++ 代码,大致代码如下:

arkts 部分:

import recorder from 'librecorder.so'
recorder.initNative()

librecorder.so 为工程中 c++ 的部分,具体可以参考项目模板中关于 c++ 的示例

napi 部分:

#include "RecorderNative.h"
#include 

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0xFF00
#define LOG_TAG "recorder"

struct AsyncCallbackInfo {
    napi_env env;
    napi_async_work asyncWork;
    napi_deferred deferred;
    int32_t resultCode = 0;
    std::string surfaceId = "";
    SampleInfo sampleInfo;
};

void DealCallBack(napi_env env, void *data)
{
    AsyncCallbackInfo *asyncCallbackInfo = static_cast(data);
    napi_value code;
    napi_create_int32(env, asyncCallbackInfo->resultCode, &code);
    napi_value surfaceId;
    napi_create_string_utf8(env, asyncCallbackInfo->surfaceId.data(), NAPI_AUTO_LENGTH, &surfaceId);
    napi_value obj;
    napi_create_object(env, &obj);

    napi_set_named_property(env, obj, "code", code);
    napi_set_named_property(env, obj, "surfaceId", surfaceId);
    napi_resolve_deferred(asyncCallbackInfo->env, asyncCallbackInfo->deferred, obj);
    napi_delete_async_work(env, asyncCallbackInfo->asyncWork);
    delete asyncCallbackInfo;
}

void SetCallBackResult(AsyncCallbackInfo *asyncCallbackInfo, int32_t code)
{
    asyncCallbackInfo->resultCode = code;
}

void SurfaceIdCallBack(AsyncCallbackInfo *asyncCallbackInfo, std::string surfaceId)
{
    asyncCallbackInfo->surfaceId = surfaceId;
}

void NativeInit(napi_env env, void *data)
{
    AsyncCallbackInfo *asyncCallbackInfo = static_cast(data);
    int32_t ret = Recorder::GetInstance().Init(asyncCallbackInfo->sampleInfo);
    if (ret != AVCODEC_SAMPLE_ERR_OK) {
        SetCallBackResult(asyncCallbackInfo, -1);
    }

    uint64_t id = 0;
    ret = OH_NativeWindow_GetSurfaceId(asyncCallbackInfo->sampleInfo.window, &id);
    if (ret != AVCODEC_SAMPLE_ERR_OK) {
        SetCallBackResult(asyncCallbackInfo, -1);
    }
    asyncCallbackInfo->surfaceId = std::to_string(id);
    SurfaceIdCallBack(asyncCallbackInfo, asyncCallbackInfo->surfaceId);
}

napi_value RecorderNative::Init(napi_env env, napi_callback_info info)
{

    SampleInfo sampleInfo;

    napi_value promise;
    napi_deferred deferred;
    napi_create_promise(env, &deferred, &promise);

    AsyncCallbackInfo *asyncCallbackInfo = new AsyncCallbackInfo();

    asyncCallbackInfo->env = env;
    asyncCallbackInfo->asyncWork = nullptr;
    asyncCallbackInfo->deferred = deferred;
    asyncCallbackInfo->resultCode = -1;
    asyncCallbackInfo->sampleInfo = sampleInfo;
    
    napi_value resourceName;
    napi_create_string_latin1(env, "recorder", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(
        env, nullptr, resourceName, [](napi_env env, void *data) { NativeInit(env, data); },
        [](napi_env env, napi_status status, void *data) { DealCallBack(env, data); }, (void *)asyncCallbackInfo,
        &asyncCallbackInfo->asyncWork);
    napi_queue_async_work(env, asyncCallbackInfo->asyncWork);
    return promise;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor classProp[] = {
        {"initNative", nullptr, RecorderNative::Init, nullptr, nullptr, nullptr, napi_default, nullptr}
    };

    return exports;
}
EXTERN_C_END

static napi_module RecorderModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "recorder",
    .nm_priv = ((void *)0),
    .reserved = {0},
};


extern "C" __attribute__((constructor)) void RegisterRecorderModule(void) { napi_module_register(&RecorderModule); }

鸿蒙这边的 napi 其实是参考的 nodejs 的,语法基本一致,这里的大致逻辑就是调用 Recorder::GetInstance().Init() 获取编码器的 surfaceid 然后通过 ts 的 promise 传递给前端

c++ 编码器部分:

int32_t Recorder::Init(SampleInfo &sampleInfo)
{
    std::lock_guard lock(mutex_);

    sampleInfo_ = sampleInfo;

    videoEncoder_ = std::make_unique();
    muxer_ = std::make_unique();
    
    videoEncoder_->Create(sampleInfo_.videoCodecMime);
    ret = muxer_->Create(sampleInfo_.outputFd);
    encContext_ = new CodecUserData;
    videoEncoder_->Config(sampleInfo_, encContext_);
    muxer_->Config(sampleInfo_);
    sampleInfo.window = sampleInfo_.window;
    releaseThread_ = nullptr;
    return AVCODEC_SAMPLE_ERR_OK;
}

其中核心的在于 videoEncoder_->Config(),这一步会将 nativewindow 赋值给 sampleInfo 结构体,然后就可以获取到nativewindow 的 surfaceid了

代码如下:

int32_t VideoEncoder::Config(SampleInfo &sampleInfo, CodecUserData *codecUserData)
{
    Configure(sampleInfo);
    OH_VideoEncoder_GetSurface(encoder_, &sampleInfo.window);
    SetCallback(codecUserData);
    OH_VideoEncoder_Prepare(encoder_);
    return AVCODEC_SAMPLE_ERR_OK;
}

到此为止,xcomponents 的 surfaceid 和编码器的 surtfaceid 都获取到了,接着就是在 arkts 层创建摄像头,并设置预览&编码输出了,这块比较简单,照着文档来就行,代码如下:

let cameraManager = camera.getCameraManager(globalThis.context)
let camerasDevices: Array = getCameraDevices(cameraManager)
let profiles: camera.CameraOutputCapability = cameraManager.getSupportedOutputCapability(camerasDevices[0],
      camera.SceneMode.NORMAL_VIDEO)

// 获取预览流profile
    let previewProfiles: Array = profiles.previewProfiles

// 获取录像流profile
    let videoProfiles: Array = profiles.videoProfiles

// Xcomponent预览流
    let XComponentPreviewProfile: camera.Profile = previewProfiles[0]

// 创建 编码器 输出对象
    encoderVideoOutput = cameraManager.createVideoOutput(videoProfile, encoderSurfaceId)

// 创建 预览流 输出对象
    XcomponentPreviewOutput = cameraManager.createPreviewOutput(XComponentPreviewProfile, this.XcomponentSurfaceId)

// 创建cameraInput对象
    cameraInput = cameraManager.createCameraInput(camerasDevices[0])

// 打开相机
    await cameraInput.open()

// 会话流程
    videoSession = cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession

// 开始配置会话
    videoSession.beginConfig()

// 把CameraInput加入到会话
    videoSession.addInput(cameraInput)

// 把 Xcomponent 预览流加入到会话
    videoSession.addOutput(XcomponentPreviewOutput)

// 把编码器录像流加入到会话
    videoSession.addOutput(encoderVideoOutput)

// 提交配置信息
    await videoSession.commitConfig()

// 会话开始
    await videoSession.start()

至此,关于预览&编码的大致流程就是这样了,整体流程其实还是很简单的,核心就是获取两个 surfaceid,然后传入到摄像头录制&预览流中即可。这里就大致讲一下思路,相信做安卓或者前端的同学都能看明白。不过这种模式的一个缺点在于无法做一些深层次的操作,例如水印、美白、瘦脸等,优点在于代码量比较少。第二篇要将的是关于如何通过 opengl 来绘制预览 & 编码 surface,未完待续~

 

 

你可能感兴趣的:(harmonyos,华为)