鸿蒙 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,未完待续~