巴延興
深圳開(kāi)鴻數(shù)字產(chǎn)業(yè)發(fā)展有限公司
資深OS框架開(kāi)發(fā)工程師
一、簡(jiǎn)介
媒體子系統(tǒng)為開(kāi)發(fā)者提供了媒體相關(guān)的很多功能,本文針對(duì)其中的視頻錄制功能做個(gè)詳細(xì)的介紹。首先,我將通過(guò)媒體子系統(tǒng)提供的視頻錄制Test代碼作為切入點(diǎn),給大家梳理一下整個(gè)錄制的流程。
二、目錄
foundation/multimedia/camera_framework
├──frameworks
│ ├── js
│ │ └── camera_napi #napi實(shí)現(xiàn)
│ │ └── src
│ │ ├── input #Camera輸入
│ │ ├── output #Camera輸出
│ │ └── session #會(huì)話管理
│ └── native #native實(shí)現(xiàn)
│ └── camera
│ ├── BUILD.gn
│ ├── src
│ │ ├── input #Camera輸入
│ │ ├── output #Camera輸出
│ │ └── session #會(huì)話管理
├── interfaces #接口定義
│ ├── inner_api #內(nèi)部native實(shí)現(xiàn)
│ │ └── native
│ │ ├── camera
│ │ │ └── include
│ │ │ ├── input
│ │ │ ├── output
│ │ │ └── session
│ └── kits #napi接口
│ └── js
│ └── camera_napi
│ ├── BUILD.gn
│ ├── include
│ │ ├── input
│ │ ├── output
│ │ └── session
│ └── @ohos.multimedia.camera.d.ts
└── services #服務(wù)端
└── camera_service
├── binder
│ ├── base
│ ├── client #IPC的客戶端
│ │ └── src
│ └── server #IPC的服務(wù)端
│ └── src
└──src
三、錄制的總體流程
? ?四、Native接口使用
在OpenAtom OpenHarmony(以下簡(jiǎn)稱(chēng)“OpenHarmony”)系統(tǒng)中,多媒體子系統(tǒng)通過(guò)N-API接口提供給上層JS調(diào)用,N-API相當(dāng)于是JS和Native之間的橋梁,在OpenHarmony源碼中,提供了C++直接調(diào)用視頻錄制功能的例子,foundation/multimedia/camera_framework/interfaces/inner_api/native/test目錄中。本文章主要參考了camera_video.cpp文件中的視頻錄制流程。 首先根據(jù)camera_video.cpp的main方法,了解下視頻錄制的主要流程代碼。
int main(int argc, char **argv)
{
......
// 創(chuàng)建CameraManager實(shí)例
sptr camManagerObj = CameraManager::GetInstance();
// 設(shè)置回調(diào)
camManagerObj->SetCallback(std::make_shared(testName));
// 獲取支持的相機(jī)設(shè)備列表
std::vector> cameraObjList = camManagerObj->GetSupportedCameras();
// 創(chuàng)建采集會(huì)話
sptr captureSession = camManagerObj->CreateCaptureSession();
// 開(kāi)始配置采集會(huì)話
captureSession->BeginConfig();
// 創(chuàng)建CameraInput
sptr captureInput = camManagerObj->CreateCameraInput(cameraObjList[0]);
sptr cameraInput = (sptr &)captureInput;
// 開(kāi)啟CameraInput
cameraInput->Open();
// 設(shè)置CameraInput的Error回調(diào)
cameraInput->SetErrorCallback(std::make_shared(testName));
// 添加CameraInput實(shí)例到采集會(huì)話中
ret = captureSession->AddInput(cameraInput);
sptr videoSurface = nullptr;
std::shared_ptr recorder = nullptr;
// 創(chuàng)建Video的Surface
videoSurface = Surface::CreateSurfaceAsConsumer();
sptr videoListener = new SurfaceListener("Video", SurfaceType::VIDEO, g_videoFd, videoSurface);
// 注冊(cè)Surface的事件監(jiān)聽(tīng)
videoSurface->RegisterConsumerListener((sptr &)videoListener);
// 視頻的配置
VideoProfile videoprofile = VideoProfile(static_cast(videoFormat), videosize, videoframerates);
// 創(chuàng)建VideoOutput實(shí)例
sptr videoOutput = camManagerObj->CreateVideoOutput(videoprofile, videoSurface);
// 設(shè)置VideoOutput的回調(diào)
((sptr &)videoOutput)->SetCallback(std::make_shared(testName));
// 添加videoOutput到采集會(huì)話中
ret = captureSession->AddOutput(videoOutput);
// 提交會(huì)話配置
ret = captureSession->CommitConfig();
// 開(kāi)始錄制
ret = ((sptr &)videoOutput)->Start();
sleep(videoPauseDuration);
MEDIA_DEBUG_LOG("Resume video recording");
// 暫停錄制
ret = ((sptr &)videoOutput)->Resume();
MEDIA_DEBUG_LOG("Wait for 5 seconds before stop");
sleep(videoCaptureDuration);
MEDIA_DEBUG_LOG("Stop video recording");
// 停止錄制
ret = ((sptr &)videoOutput)->Stop();
MEDIA_DEBUG_LOG("Closing the session");
// 停止采集會(huì)話
ret = captureSession->Stop();
MEDIA_DEBUG_LOG("Releasing the session");
// 釋放會(huì)話采集
captureSession->Release();
// Close video file
TestUtils::SaveVideoFile(nullptr, 0, VideoSaveMode::CLOSE, g_videoFd);
cameraInput->Release();
camManagerObj->SetCallback(nullptr);
return 0;
}
以上是視頻錄制的整體流程,其過(guò)程主要通過(guò)Camera模塊支持的能力來(lái)實(shí)現(xiàn),其中涉及幾個(gè)重要的類(lèi):CaptureSession、CameraInput、VideoOutput。CaptureSession是整個(gè)過(guò)程的控制者,CameraInput和VideoOutput相當(dāng)于是設(shè)備的輸入和輸出。
五、調(diào)用流程
? ? ? 左右滑動(dòng)查看更多 ?后續(xù)主要針對(duì)上面的調(diào)用流程,梳理具體的調(diào)用流程,方便我們對(duì)了解視頻錄制的整理架構(gòu)有一個(gè)更加深入的了解。 1. 創(chuàng)建CameraManager實(shí)例通過(guò)CameraManager::GetInstance()獲取CameraManager的實(shí)例,后續(xù)的一些接口都是通過(guò)該實(shí)例進(jìn)行調(diào)用的。GetInstance使用了單例模式,在OpenHarmony代碼中這種方式很常見(jiàn)。
sptr &CameraManager::GetInstance()
{
if (CameraManager::cameraManager_ == nullptr) {
MEDIA_INFO_LOG("Initializing camera manager for first time!");
CameraManager::cameraManager_ = new(std::nothrow) CameraManager();
if (CameraManager::cameraManager_ == nullptr) {
MEDIA_ERR_LOG("CameraManager::GetInstance failed to new CameraManager");
}
}
return CameraManager::cameraManager_;
}
2. 獲取支持的相機(jī)設(shè)備列表通過(guò)調(diào)用CameraManager的GetSupportedCameras()接口,獲取設(shè)備支持的CameraDevice列表。跟蹤代碼可以發(fā)現(xiàn)serviceProxy_->GetCameras最終會(huì)調(diào)用到Camera服務(wù)端的對(duì)應(yīng)接口。
std::vector> CameraManager::GetSupportedCameras()
{
CAMERA_SYNC_TRACE;
std::lock_guard lock(mutex_);
std::vector cameraIds;
std::vector> cameraAbilityList;
int32_t retCode = -1;
sptr cameraObj = nullptr;
int32_t index = 0;
if (cameraObjList.size() > 0) {
cameraObjList.clear();
}
if (serviceProxy_ == nullptr) {
MEDIA_ERR_LOG("CameraManager::GetCameras serviceProxy_ is null, returning empty list!");
return cameraObjList;
}
std::vector> supportedCameras;
retCode = serviceProxy_->GetCameras(cameraIds, cameraAbilityList);
if (retCode == CAMERA_OK) {
for (auto& it : cameraIds) {
cameraObj = new(std::nothrow) CameraDevice(it, cameraAbilityList[index++]);
if (cameraObj == nullptr) {
MEDIA_ERR_LOG("CameraManager::GetCameras new CameraDevice failed for id={public}%s", it.c_str());
continue;
}
supportedCameras.emplace_back(cameraObj);
}
} else {
MEDIA_ERR_LOG("CameraManager::GetCameras failed!, retCode: %{public}d", retCode);
}
ChooseDeFaultCameras(supportedCameras);
return cameraObjList;
}
3. 創(chuàng)建采集會(huì)話下面是比較重要的環(huán)節(jié),通過(guò)調(diào)用CameraManager的CreateCaptureSession接口創(chuàng)建采集會(huì)話。CameraManager創(chuàng)建采集會(huì)話,是通過(guò)serviceProxy_->CreateCaptureSession方式進(jìn)行調(diào)用,這里涉及到了OpenHarmony中的IPC的調(diào)用,serviceProxy_是遠(yuǎn)端服務(wù)在本地的代理,通過(guò)這個(gè)代理可以調(diào)用到具體的服務(wù)端,這里是HCameraService。
sptr CameraManager::CreateCaptureSession()
{
CAMERA_SYNC_TRACE;
sptr captureSession = nullptr;
sptr result = nullptr;
int32_t retCode = CAMERA_OK;
if (serviceProxy_ == nullptr) {
MEDIA_ERR_LOG("CameraManager::CreateCaptureSession serviceProxy_ is null");
return nullptr;
}
retCode = serviceProxy_->CreateCaptureSession(captureSession);
if (retCode == CAMERA_OK && captureSession != nullptr) {
result = new(std::nothrow) CaptureSession(captureSession);
if (result == nullptr) {
MEDIA_ERR_LOG("Failed to new CaptureSession");
}
} else {
MEDIA_ERR_LOG("Failed to get capture session object from hcamera service!, %{public}d", retCode);
}
return result;
}
代碼最終來(lái)到HCameraService::CreateCaptureSession中,該方法中new了一個(gè)HCaptureSession對(duì)象,并且將該對(duì)象傳遞給了參數(shù)session,所以前面的captureSession對(duì)象就是這里new出來(lái)的HCaptureSession,前面的CameraManager的CreateCaptureSession()方法中將captureSession封裝成CaptureSession對(duì)象返回給應(yīng)用層使用。
int32_t HCameraService::CreateCaptureSession(sptr &session)
{
CAMERA_SYNC_TRACE;
sptr captureSession;
if (streamOperatorCallback_ == nullptr) {
streamOperatorCallback_ = new(std::nothrow) StreamOperatorCallback();
if (streamOperatorCallback_ == nullptr) {
MEDIA_ERR_LOG("HCameraService::CreateCaptureSession streamOperatorCallback_ allocation failed");
return CAMERA_ALLOC_ERROR;
}
}
std::lock_guard lock(mutex_);
OHOS::AccessTokenID callerToken = IPCSkeleton::GetCallingTokenID();
captureSession = new(std::nothrow) HCaptureSession(cameraHostManager_, streamOperatorCallback_, callerToken);
if (captureSession == nullptr) {
MEDIA_ERR_LOG("HCameraService::CreateCaptureSession HCaptureSession allocation failed");
return CAMERA_ALLOC_ERROR;
}
session = captureSession;
return CAMERA_OK;
}
4. 開(kāi)始配置采集會(huì)話調(diào)用CaptureSession的BeginConfig進(jìn)行采集會(huì)話的配置工作。這個(gè)工作最終調(diào)用到被封裝的HCaptureSession中。
int32_t HCaptureSession::BeginConfig()
{
CAMERA_SYNC_TRACE;
if (curState_ == CaptureSessionState::SESSION_CONFIG_INPROGRESS) {
MEDIA_ERR_LOG("HCaptureSession::BeginConfig Already in config inprogress state!");
return CAMERA_INVALID_STATE;
}
std::lock_guard lock(sessionLock_);
prevState_ = curState_;
curState_ = CaptureSessionState::SESSION_CONFIG_INPROGRESS;
tempCameraDevices_.clear();
tempStreams_.clear();
deletedStreamIds_.clear();
return CAMERA_OK;
}
5. 創(chuàng)建CameraInput應(yīng)用層通過(guò)camManagerObj->CreateCameraInput(cameraObjList[0])的方式進(jìn)行CameraInput的創(chuàng)建,cameraObjList[0]就是前面獲取支持設(shè)備的第一個(gè)。根據(jù)CameraDevice創(chuàng)建對(duì)應(yīng)的CameraInput對(duì)象。
sptr CameraManager::CreateCameraInput(sptr &camera)
{
CAMERA_SYNC_TRACE;
sptr cameraInput = nullptr;
sptr deviceObj = nullptr;
if (camera != nullptr) {
deviceObj = CreateCameraDevice(camera->GetID());
if (deviceObj != nullptr) {
cameraInput = new(std::nothrow) CameraInput(deviceObj, camera);
if (cameraInput == nullptr) {
MEDIA_ERR_LOG("failed to new CameraInput Returning null in CreateCameraInput");
return cameraInput;
}
} else {
MEDIA_ERR_LOG("Returning null in CreateCameraInput");
}
} else {
MEDIA_ERR_LOG("CameraManager: Camera object is null");
}
return cameraInput;
}
6. 開(kāi)啟CameraInput調(diào)用了CameraInput的Open方法,進(jìn)行輸入設(shè)備的啟動(dòng)打開(kāi)。
void CameraInput::Open()
{
int32_t retCode = deviceObj_->Open();
if (retCode != CAMERA_OK) {
MEDIA_ERR_LOG("Failed to open Camera Input, retCode: %{public}d", retCode);
}
}
7. 添加CameraInput實(shí)例到采集會(huì)話中通過(guò)調(diào)用captureSession的AddInput方法,將創(chuàng)建的CameraInput對(duì)象添加到采集會(huì)話的輸入中,這樣采集會(huì)話就知道采集輸入的設(shè)備。
int32_t CaptureSession::AddInput(sptr &input)
{
CAMERA_SYNC_TRACE;
if (input == nullptr) {
MEDIA_ERR_LOG("CaptureSession::AddInput input is null");
return CAMERA_INVALID_ARG;
}
input->SetSession(this);
inputDevice_ = input;
return captureSession_->AddInput(((sptr &)input)->GetCameraDevice());
}
最終調(diào)用到HCaptureSession的AddInput方法,該方法中核心的代碼是tempCameraDevices_.emplace_back(localCameraDevice),將需要添加的CameraDevice插入到tempCameraDevices_容器中。
int32_t HCaptureSession::AddInput(sptr cameraDevice)
{
CAMERA_SYNC_TRACE;
sptr localCameraDevice = nullptr;
if (cameraDevice == nullptr) {
MEDIA_ERR_LOG("HCaptureSession::AddInput cameraDevice is null");
return CAMERA_INVALID_ARG;
}
if (curState_ != CaptureSessionState::SESSION_CONFIG_INPROGRESS) {
MEDIA_ERR_LOG("HCaptureSession::AddInput Need to call BeginConfig before adding input");
return CAMERA_INVALID_STATE;
}
if (!tempCameraDevices_.empty() || (cameraDevice_ != nullptr && !cameraDevice_->IsReleaseCameraDevice())) {
MEDIA_ERR_LOG("HCaptureSession::AddInput Only one input is supported");
return CAMERA_INVALID_SESSION_CFG;
}
localCameraDevice = static_cast(cameraDevice.GetRefPtr());*>
if (cameraDevice_ == localCameraDevice) {
cameraDevice_->SetReleaseCameraDevice(false);
} else {
tempCameraDevices_.emplace_back(localCameraDevice);
CAMERA_SYSEVENT_STATISTIC(CreateMsg("CaptureSession::AddInput"));
}
sptr streamOperator;
int32_t rc = localCameraDevice->GetStreamOperator(streamOperatorCallback_, streamOperator);
if (rc != CAMERA_OK) {
MEDIA_ERR_LOG("HCaptureSession::GetCameraDevice GetStreamOperator returned %{public}d", rc);
localCameraDevice->Close();
return rc;
}
return CAMERA_OK;
}
8. 創(chuàng)建Video的Surface通過(guò)Surface::CreateSurfaceAsConsumer創(chuàng)建Surface。
sptr Surface::string name, bool isShared)
{
sptr surf = new ConsumerSurface(name, isShared);
GSError ret = surf->Init();
if (ret != GSERROR_OK) {
BLOGE("Failure, Reason: consumer surf init failed");
return nullptr;
}
return surf;
}
9. 創(chuàng)建VideoOutput實(shí)例通過(guò)調(diào)用CameraManager的CreateVideoOutput來(lái)創(chuàng)建VideoOutput實(shí)例。
sptr CameraManager::CreateVideoOutput(VideoProfile &profile, sptr &surface)
{
CAMERA_SYNC_TRACE;
sptr streamRepeat = nullptr;
sptr result = nullptr;
int32_t retCode = CAMERA_OK;
camera_format_t metaFormat;
metaFormat = GetCameraMetadataFormat(profile.GetCameraFormat());
retCode = serviceProxy_->CreateVideoOutput(surface->GetProducer(), metaFormat,
profile.GetSize().width, profile.GetSize().height, streamRepeat);
if (retCode == CAMERA_OK) {
result = new(std::nothrow) VideoOutput(streamRepeat);
if (result == nullptr) {
MEDIA_ERR_LOG("Failed to new VideoOutput");
} else {
std::vector videoFrameRates = profile.GetFrameRates();
if (videoFrameRates.size() >= 2) { // vaild frame rate range length is 2
result->SetFrameRateRange(videoFrameRates[0], videoFrameRates[1]);
}
POWERMGR_SYSEVENT_CAMERA_CONFIG(VIDEO,
profile.GetSize().width,
profile.GetSize().height);
}
} else {
MEDIA_ERR_LOG("VideoOutpout: Failed to get stream repeat object from hcamera service! %{public}d", retCode);
}
return result;
}
該方法中通過(guò)IPC的調(diào)用最終調(diào)用到了HCameraService的CreateVideoOutput(surface->GetProducer(), format, streamRepeat)。
int32_t HCameraService::CreateVideoOutput(const sptr &producer, int32_t format,
int32_t width, int32_t height,
sptr &videoOutput)
{
CAMERA_SYNC_TRACE;
sptr streamRepeatVideo;
if ((producer == nullptr) || (width == 0) || (height == 0)) {
MEDIA_ERR_LOG("HCameraService::CreateVideoOutput producer is null");
return CAMERA_INVALID_ARG;
}
streamRepeatVideo = new(std::nothrow) HStreamRepeat(producer, format, width, height, true);
if (streamRepeatVideo == nullptr) {
MEDIA_ERR_LOG("HCameraService::CreateVideoOutput HStreamRepeat allocation failed");
return CAMERA_ALLOC_ERROR;
}
POWERMGR_SYSEVENT_CAMERA_CONFIG(VIDEO, producer->GetDefaultWidth(),
producer->GetDefaultHeight());
videoOutput = streamRepeatVideo;
return CAMERA_OK;
}
HCameraService的CreateVideoOutput方法中主要?jiǎng)?chuàng)建了HStreamRepeat,并且通過(guò)參數(shù)傳遞給前面的CameraManager使用,CameraManager通過(guò)傳遞的HStreamRepeat對(duì)象,進(jìn)行封裝,創(chuàng)建出VideoOutput對(duì)象。
10. 添加videoOutput到采集會(huì)話中,并且提交采集會(huì)話該步驟類(lèi)似添加CameraInput到采集會(huì)話的過(guò)程,可以參考前面的流程。
11. 開(kāi)始錄制通過(guò)調(diào)用VideoOutput的Start進(jìn)行錄制的操作。
int32_t VideoOutput::Start()
{
return static_cast(GetStream().GetRefPtr())->Start();
}
該方法中會(huì)調(diào)用到HStreamRepeat的Start方法。
int32_t HStreamRepeat::Start()
{
CAMERA_SYNC_TRACE;
if (streamOperator_ == nullptr) {
return CAMERA_INVALID_STATE;
}
if (curCaptureID_ != 0) {
MEDIA_ERR_LOG("HStreamRepeat::Start, Already started with captureID: %{public}d", curCaptureID_);
return CAMERA_INVALID_STATE;
}
int32_t ret = AllocateCaptureId(curCaptureID_);
if (ret != CAMERA_OK) {
MEDIA_ERR_LOG("HStreamRepeat::Start Failed to allocate a captureId");
return ret;
}
std::vector ability;
OHOS::ConvertMetadataToVec(cameraAbility_, ability);
CaptureInfo captureInfo;
captureInfo.streamIds_ = {streamId_};
captureInfo.captureSetting_ = ability;
captureInfo.enableShutterCallback_ = false;
MEDIA_INFO_LOG("HStreamRepeat::Start Starting with capture ID: %{public}d", curCaptureID_);
CamRetCode rc = (CamRetCode)(streamOperator_->Capture(curCaptureID_, captureInfo, true));
if (rc != HDI::NO_ERROR) {
ReleaseCaptureId(curCaptureID_);
curCaptureID_ = 0;
MEDIA_ERR_LOG("HStreamRepeat::Start Failed with error Code:%{public}d", rc);
ret = HdiToServiceError(rc);
}
return ret;
}
核心的代碼是streamOperator_->Capture,其中最后一個(gè)參數(shù)true,表示采集連續(xù)數(shù)據(jù)。
12. 錄制結(jié)束,保存錄制文件
六、總結(jié)
本文主要對(duì)OpenHarmony 3.2 Beta多媒體子系統(tǒng)的視頻錄制進(jìn)行介紹,首先梳理了整體的錄制流程,然后對(duì)錄制過(guò)程中的主要步驟進(jìn)行了詳細(xì)地分析。視頻錄制主要分為以下幾個(gè)步驟:(1) 獲取CameraManager實(shí)例。(2) 創(chuàng)建采集會(huì)話CaptureSession。(3) 創(chuàng)建CameraInput實(shí)例,并且將輸入設(shè)備添加到CaptureSession中。(4) 創(chuàng)建Video錄制需要的Surface。(5) 創(chuàng)建VideoOutput實(shí)例,并且將輸出添加到CaptureSession中。(6) 提交采集會(huì)話的配置。(7) 調(diào)用VideoOutput的Start方法,進(jìn)行視頻的錄制。(8) 錄制結(jié)束,保存錄制的文件。 關(guān)于OpenHarmony 3.2 Beta多媒體系列開(kāi)發(fā),我之前還分享過(guò)《OpenHarmony 3.2 Beta源碼分析之MediaLibrary》《OpenHarmony 3.2 Beta多媒體系列——音視頻播放框架》《OpenHarmony 3.2 Beta多媒體系列——音視頻播放gstreamer》這幾篇文章,歡迎感興趣的開(kāi)發(fā)者進(jìn)行閱讀。THE END
推薦閱讀
點(diǎn)擊圖片即可閱讀
關(guān)注深開(kāi)鴻
了解更多資訊
深開(kāi)鴻公眾號(hào)
深開(kāi)鴻視頻號(hào)
深圳開(kāi)鴻數(shù)字產(chǎn)業(yè)發(fā)展有限公司(簡(jiǎn)稱(chēng)“深開(kāi)鴻”)于2021年成立于中國(guó)深圳,以數(shù)字化、智慧化改變?nèi)祟?lèi)的生產(chǎn)和生活方式為愿景,專(zhuān)注于自主軟件根技術(shù)的研發(fā)與持續(xù)創(chuàng)新,致力于打造萬(wàn)物智聯(lián)核心技術(shù)、定義萬(wàn)物智聯(lián)標(biāo)準(zhǔn)、引領(lǐng)萬(wàn)物智聯(lián)時(shí)代發(fā)展。
深開(kāi)鴻基于OpenHarmony,創(chuàng)新打造互通互聯(lián)互享的KaihongOS數(shù)字底座,上承可視可管可控的超級(jí)設(shè)備管理平臺(tái),靈活擴(kuò)展,柔性組合,聚合成全場(chǎng)景超級(jí)設(shè)備解決方案,實(shí)現(xiàn)更大范圍的以軟件定義硬件,引領(lǐng)智慧基建、智慧康養(yǎng)、智慧能源、智慧交通、智慧制造、智慧政務(wù)、智慧金融、智慧教育等多個(gè)行業(yè)變革,賦能、賦智、賦值千行百業(yè)的數(shù)智化轉(zhuǎn)型。
從開(kāi)源中來(lái),到行業(yè)中去,深開(kāi)鴻以構(gòu)筑行業(yè)數(shù)字化生態(tài)、培養(yǎng)生態(tài)人才為己任,持續(xù)突破行業(yè)邊界,立志成為萬(wàn)物智聯(lián)時(shí)代的“國(guó)之重器”。
以數(shù)字化、智慧化改變?nèi)祟?lèi)的生產(chǎn)和生活方式
點(diǎn)擊在看和點(diǎn)贊,與更多的美好相遇 ↓
原文標(biāo)題:OpenHarmony 3.2 Beta多媒體系列——視頻錄制
文章出處:【微信公眾號(hào):深開(kāi)鴻】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
深開(kāi)鴻
+關(guān)注
關(guān)注
1文章
333瀏覽量
2202
原文標(biāo)題:OpenHarmony 3.2 Beta多媒體系列——視頻錄制
文章出處:【微信號(hào):gh_15d2f062a168,微信公眾號(hào):深開(kāi)鴻】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論