From f81247f751fd691c06bce62e3f7a631b8e4a4174 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 29 Jul 2026 16:09:17 +0800 Subject: [PATCH 1/4] fix(harmony): save raw video snapshots to gallery --- .../entry/src/main/cpp/origin_video_data.cpp | 163 +++++++++++++++--- .../entry/src/main/cpp/origin_video_data.h | 9 +- .../cpp/types/librawdataprocessing/index.d.ts | 12 +- .../ets/pages/advance/OriginVideoDataWrap.ets | 108 +++++++++++- .../main/resources/base/element/string.json | 12 ++ .../main/resources/en_US/element/string.json | 12 ++ .../main/resources/zh_CN/element/string.json | 12 ++ 7 files changed, 293 insertions(+), 35 deletions(-) diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.cpp b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.cpp index 05f6c7f87..298164af7 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.cpp +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.cpp @@ -6,37 +6,131 @@ #include "origin_video_data.h" #include +#include +#include + +namespace { +struct SnapshotFrame { + std::vector data; + int width; + int height; + int rotation; + int errorCode; +}; + +void SetInt32Property(napi_env env, napi_value object, const char *name, int value) { + napi_value property; + if (napi_create_int32(env, value, &property) == napi_ok) { + napi_set_named_property(env, object, name, property); + } +} +} // namespace OriginVideoData::OriginVideoData(uintptr_t rtcEngineHandler) - : rtcEngine_(reinterpret_cast(rtcEngineHandler)), env_(nullptr), wrapper_(nullptr),takeSnapshot_(false) {} - -OriginVideoData::~OriginVideoData() { napi_delete_reference(env_, wrapper_); } + : rtcEngine_(reinterpret_cast(rtcEngineHandler)), env_(nullptr), wrapper_(nullptr), + takeSnapshot_(false), snapshotCallback_(nullptr) {} + +OriginVideoData::~OriginVideoData() { + takeSnapshot_.store(false); + { + std::lock_guard lock(snapshotMutex_); + if (snapshotCallback_ != nullptr) { + napi_release_threadsafe_function(snapshotCallback_, napi_tsfn_abort); + snapshotCallback_ = nullptr; + } + } + if (env_ != nullptr && wrapper_ != nullptr) { + napi_delete_reference(env_, wrapper_); + } +} bool OriginVideoData::onCaptureVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE sourceType, VideoFrame &videoFrame) { - if (takeSnapshot_) { - uint8_t *buffer = videoFrame.yBuffer; - // real path: /data/app/el2/100/base/io.agora.hmos.apiexample/haps/entry/files/capture.i420 - // play file: ffplay -f rawvideo -pixel_format yuv420p -video_size 1280x720 /Users/xcz/Downloads/capture.i420 - SaveI420Buffer(buffer, videoFrame.width, videoFrame.height, - "/data/storage/el2/base/haps/entry/files/capture.i420"); - takeSnapshot_ = false; + if (!takeSnapshot_.exchange(false)) { + return true; + } + + auto *snapshot = new SnapshotFrame{{}, videoFrame.width, videoFrame.height, videoFrame.rotation, 0}; + const bool invalidFrame = videoFrame.width <= 0 || videoFrame.height <= 0 || + videoFrame.width % 2 != 0 || videoFrame.height % 2 != 0 || + videoFrame.yBuffer == nullptr || videoFrame.uBuffer == nullptr || + videoFrame.vBuffer == nullptr || videoFrame.yStride < videoFrame.width || + videoFrame.uStride < videoFrame.width / 2 || videoFrame.vStride < videoFrame.width / 2; + if (invalidFrame) { + snapshot->errorCode = -1; + } else { + const int width = videoFrame.width; + const int height = videoFrame.height; + const int chromaWidth = width / 2; + const int chromaHeight = height / 2; + snapshot->data.resize(width * height + chromaWidth * chromaHeight * 2); + + uint8_t *destination = snapshot->data.data(); + for (int row = 0; row < height; ++row) { + std::memcpy(destination + row * width, videoFrame.yBuffer + row * videoFrame.yStride, width); + } + + uint8_t *chromaDestination = destination + width * height; + for (int row = 0; row < chromaHeight; ++row) { + const uint8_t *uRow = videoFrame.uBuffer + row * videoFrame.uStride; + const uint8_t *vRow = videoFrame.vBuffer + row * videoFrame.vStride; + for (int column = 0; column < chromaWidth; ++column) { + chromaDestination[row * width + column * 2] = vRow[column]; + chromaDestination[row * width + column * 2 + 1] = uRow[column]; + } + } + } + + napi_threadsafe_function callback = nullptr; + { + std::lock_guard lock(snapshotMutex_); + callback = snapshotCallback_; + snapshotCallback_ = nullptr; + } + if (callback == nullptr) { + delete snapshot; + return true; + } + + napi_status status = napi_call_threadsafe_function(callback, snapshot, napi_tsfn_nonblocking); + napi_release_threadsafe_function(callback, napi_tsfn_release); + if (status != napi_ok) { + delete snapshot; } return true; } -void OriginVideoData::SaveI420Buffer(const uint8_t *buf, int width, int height, std::string filename) { - AG_INFO("OriginVideoData::SaveI420Buffer takeSnapshot -- width=%{public}d, height=%{public}d, filename=%{public}s", width, height, - filename.c_str()); - - FILE *dump_file_ = fopen(filename.c_str(), "w"); - if (!dump_file_) { +void OriginVideoData::CallSnapshotCallback(napi_env env, napi_value callback, + [[maybe_unused]] void *context, void *data) { + auto *snapshot = static_cast(data); + if (snapshot == nullptr) { return; } - int size = width * height * 3 / 2; - fwrite(buf, sizeof(uint8_t), size, dump_file_); - fflush(dump_file_); - fclose(dump_file_); + if (env == nullptr || callback == nullptr) { + delete snapshot; + return; + } + + napi_value result; + napi_create_object(env, &result); + SetInt32Property(env, result, "width", snapshot->width); + SetInt32Property(env, result, "height", snapshot->height); + SetInt32Property(env, result, "rotation", snapshot->rotation); + SetInt32Property(env, result, "errorCode", snapshot->errorCode); + + void *bufferData = nullptr; + napi_value buffer; + if (napi_create_arraybuffer(env, snapshot->data.size(), &bufferData, &buffer) == napi_ok) { + if (!snapshot->data.empty()) { + std::memcpy(bufferData, snapshot->data.data(), snapshot->data.size()); + } + napi_set_named_property(env, result, "data", buffer); + } + + napi_value undefined; + napi_get_undefined(env, &undefined); + napi_call_function(env, undefined, callback, 1, &result, nullptr); + delete snapshot; } OriginVideoData::VIDEO_FRAME_PROCESS_MODE OriginVideoData::getVideoFrameProcessMode() { return PROCESS_MODE_READ_ONLY; } @@ -142,16 +236,39 @@ napi_value OriginVideoData::Enable(napi_env env, napi_callback_info info) { } napi_value OriginVideoData::TakeSnapshot(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; napi_value jsThis; - assert(napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr) == napi_ok); + assert(napi_get_cb_info(env, info, &argc, args, &jsThis, nullptr) == napi_ok); OriginVideoData *obj; // 通过napi_unwrap将jsThis之前绑定的C++对象取出,并对其进行操作 assert(napi_unwrap(env, jsThis, reinterpret_cast(&obj)) == napi_ok); - obj->takeSnapshot_ = true; + int result = 0; + napi_valuetype callbackType = napi_undefined; + if (argc != 1 || napi_typeof(env, args[0], &callbackType) != napi_ok || callbackType != napi_function) { + result = -1; + } else { + std::lock_guard lock(obj->snapshotMutex_); + if (obj->takeSnapshot_.load() || obj->snapshotCallback_ != nullptr) { + result = -2; + } else { + napi_value resourceName; + napi_create_string_utf8(env, "OriginVideoDataSnapshot", NAPI_AUTO_LENGTH, &resourceName); + napi_status status = napi_create_threadsafe_function( + env, args[0], nullptr, resourceName, 1, 1, nullptr, nullptr, nullptr, + CallSnapshotCallback, &obj->snapshotCallback_); + if (status == napi_ok) { + obj->takeSnapshot_.store(true); + } else { + obj->snapshotCallback_ = nullptr; + result = -3; + } + } + } napi_value num; - assert(napi_create_int32(env, 0, &num) == napi_ok); + assert(napi_create_int32(env, result, &num) == napi_ok); return num; } diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.h b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.h index 995364e24..6ab2a7592 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.h +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/origin_video_data.h @@ -12,6 +12,8 @@ #include "utils.h" #include "napi/native_api.h" #include "IAgoraRtcEngine.h" +#include +#include static thread_local napi_ref g_origin_video_data_ref = nullptr; @@ -25,11 +27,10 @@ class OriginVideoData : public agora::media::IVideoFrameObserver { explicit OriginVideoData(); ~OriginVideoData(); - void SaveI420Buffer(const uint8_t *buf, int width, int height, std::string filename); - static napi_value New(napi_env env, napi_callback_info info); static napi_value Enable(napi_env env, napi_callback_info info); static napi_value TakeSnapshot(napi_env env, napi_callback_info info); + static void CallSnapshotCallback(napi_env env, napi_value callback, void *context, void *data); // IVideoFrameObserver bool onCaptureVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE sourceType, VideoFrame &videoFrame); @@ -50,7 +51,9 @@ class OriginVideoData : public agora::media::IVideoFrameObserver { agora::rtc::IRtcEngine *rtcEngine_; napi_env env_; napi_ref wrapper_; - bool takeSnapshot_; + std::atomic takeSnapshot_; + std::mutex snapshotMutex_; + napi_threadsafe_function snapshotCallback_; }; #endif // APIEXAMPLE_ORIGIN_VIDEO_DATA_H diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts index 4261af2e8..d793e3202 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts @@ -11,11 +11,19 @@ export class CustomAudioRender { export class OriginVideoData { constructor(arg: bigint); enable: (enable:boolean) => number; - takeSnapshot: () => number; + takeSnapshot: (callback: (frame: SnapshotFrame) => void) => number; +} + +export interface SnapshotFrame { + data: ArrayBuffer; + width: number; + height: number; + rotation: number; + errorCode: number; } export class MediaMetadata { constructor(arg: bigint); enable: (enable:boolean) => number; send: (data: string) => number; -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/OriginVideoDataWrap.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/OriginVideoDataWrap.ets index a85797bd0..ad16d74fd 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/OriginVideoDataWrap.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/OriginVideoDataWrap.ets @@ -5,6 +5,9 @@ import { VideoCanvas, VideoEncoderConfiguration } from '@shengwang/rtc-full'; +import { image } from '@kit.ImageKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { BusinessError } from '@kit.BasicServicesKit'; import { TitleBar } from '../../common/TitleBar'; import Logger from '../../util/Logger'; import ShowToast from '../../util/ShowToast'; @@ -12,7 +15,7 @@ import { AppID } from '../../common/KeyCenter'; import PermissionHelper from '../../util/PermissionHelper'; import { common } from '@kit.AbilityKit'; import { GlobalInfo } from '../Data'; -import { OriginVideoData } from 'librawdataprocessing.so'; +import { OriginVideoData, SnapshotFrame } from 'librawdataprocessing.so'; const TAG: string = 'OriginVideoData' @@ -26,6 +29,7 @@ struct OriginVideoDataWrap { @State isJoin: boolean = false @State localUid: number = 0 @State remoteUid: number = 0 + @State isSavingSnapshot: boolean = false aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear') @@ -74,6 +78,8 @@ struct OriginVideoDataWrap { } aboutToDisappear(): void { + this.native?.enable(false) + this.native = undefined if (this.rtcEngine != undefined) { if (this.isJoin) { this.rtcEngine.stopPreview(); @@ -88,6 +94,83 @@ struct OriginVideoDataWrap { } } + private async takeSnapshot(): Promise { + if (this.native == undefined || this.isSavingSnapshot) { + return + } + this.isSavingSnapshot = true + let result = this.native.takeSnapshot((frame: SnapshotFrame) => { + this.saveSnapshot(frame) + }) + if (result != 0) { + this.isSavingSnapshot = false + Logger.error(TAG, `takeSnapshot failed: ${result}`) + ShowToast.longToast($r('app.string.screenshot_save_failed')) + } + } + + private async saveSnapshot(frame: SnapshotFrame): Promise { + let pixelMap: image.PixelMap | undefined = undefined + let imagePacker: image.ImagePacker | undefined = undefined + let photoHelper: photoAccessHelper.PhotoAccessHelper | undefined = undefined + try { + if (frame.errorCode != 0 || frame.width <= 0 || frame.height <= 0 || frame.data.byteLength == 0) { + throw new Error(`Invalid snapshot frame: ${frame.errorCode}`) + } + + pixelMap = await image.createPixelMap(frame.data, { + size: { width: frame.width, height: frame.height }, + srcPixelFormat: image.PixelMapFormat.NV21, + pixelFormat: image.PixelMapFormat.RGBA_8888, + editable: true + }) + if (frame.rotation != 0) { + await pixelMap.rotate(frame.rotation) + } + + imagePacker = image.createImagePacker() + let jpegData = await imagePacker.packToData(pixelMap, { format: 'image/jpeg', quality: 90 }) + let context = getContext(this) as common.UIAbilityContext; + photoHelper = photoAccessHelper.getPhotoAccessHelper(context) + let request = photoAccessHelper.MediaAssetChangeRequest.createAssetRequest( + context, + photoAccessHelper.PhotoType.IMAGE, + 'jpg', + { title: `IMG_AGORA_${Date.now()}` } + ) + request.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, jpegData) + await photoHelper.applyChanges(request) + ShowToast.longToast($r('app.string.screenshot_saved')) + } catch (error) { + let err = error as BusinessError + Logger.error(TAG, `saveSnapshot failed. Code: ${err.code}, message: ${err.message}`) + ShowToast.longToast($r('app.string.screenshot_save_failed')) + } finally { + this.isSavingSnapshot = false + if (imagePacker != undefined) { + try { + await imagePacker.release() + } catch (error) { + Logger.error(TAG, `release ImagePacker failed: ${(error as BusinessError).message}`) + } + } + if (pixelMap != undefined) { + try { + await pixelMap.release() + } catch (error) { + Logger.error(TAG, `release PixelMap failed: ${(error as BusinessError).message}`) + } + } + if (photoHelper != undefined) { + try { + await photoHelper.release() + } catch (error) { + Logger.error(TAG, `release PhotoAccessHelper failed: ${(error as BusinessError).message}`) + } + } + } + } + build() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.End, justifyContent: FlexAlign.Start }) { TitleBar({ @@ -131,12 +214,23 @@ struct OriginVideoDataWrap { } .align(Alignment.TopStart) .flexGrow(1) - Button($r('app.string.screenshot')) - .id('button_screenshot') - .enabled(this.isJoin) - .onClick(() => { - this.native?.takeSnapshot(); + Row() { + SaveButton({ + icon: SaveIconStyle.LINES, + text: SaveDescription.SAVE_TO_GALLERY, + buttonType: ButtonType.Capsule }) + .id('button_screenshot') + .enabled(this.isJoin && !this.isSavingSnapshot) + .onClick((event: ClickEvent, result: SaveButtonOnClickResult) => { + if (result == SaveButtonOnClickResult.SUCCESS) { + this.takeSnapshot() + } else { + Logger.error(TAG, `SaveButton authorization failed: ${result}`) + ShowToast.longToast($r('app.string.screenshot_permission_denied')) + } + }) + } .margin(12) .flexGrow(0) Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { @@ -194,4 +288,4 @@ struct OriginVideoDataWrap { } .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json index df4eed4dc..e9d038dc1 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json @@ -68,6 +68,18 @@ "name": "screenshot", "value": "Screenshot" }, + { + "name": "screenshot_saved", + "value": "Screenshot saved. You can view it in Photos." + }, + { + "name": "screenshot_save_failed", + "value": "Failed to save screenshot." + }, + { + "name": "screenshot_permission_denied", + "value": "Unable to get permission to save the screenshot." + }, { "name": "ex_channel_name", "value": "ExChannelName" diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json index cad7f7418..2dc2a3746 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json @@ -68,6 +68,18 @@ "name": "screenshot", "value": "Screenshot" }, + { + "name": "screenshot_saved", + "value": "Screenshot saved. You can view it in Photos." + }, + { + "name": "screenshot_save_failed", + "value": "Failed to save screenshot." + }, + { + "name": "screenshot_permission_denied", + "value": "Unable to get permission to save the screenshot." + }, { "name": "ex_channel_name", "value": "ExChannelName" diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json index d535e55ae..eea7479a7 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json @@ -68,6 +68,18 @@ "name": "screenshot", "value": "截图" }, + { + "name": "screenshot_saved", + "value": "截图已保存,可在相册中查看" + }, + { + "name": "screenshot_save_failed", + "value": "截图保存失败" + }, + { + "name": "screenshot_permission_denied", + "value": "无法获取截图保存授权" + }, { "name": "ex_channel_name", "value": "Ex频道名" From 9192b6d402416108580f3a65afa90c8b457e187f Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 29 Jul 2026 20:03:46 +0800 Subject: [PATCH 2/4] fix(harmony): refine live streaming settings --- .../main/ets/pages/advance/LiveStreaming.ets | 47 ++----------------- .../main/resources/base/element/string.json | 4 -- .../main/resources/en_US/element/string.json | 4 -- .../main/resources/zh_CN/element/string.json | 6 +-- 4 files changed, 5 insertions(+), 56 deletions(-) diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LiveStreaming.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LiveStreaming.ets index 6c636e925..b5d6a0417 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LiveStreaming.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LiveStreaming.ets @@ -49,7 +49,6 @@ struct LiveStreaming { @State lowLegacyLiving: boolean = false @State pullLowStream: boolean = false @State remoteFallbackIndex: number = 1 - @State isRemoteUserHighPriority: boolean = false @State isVideoImageSourceReady: boolean = false @State isVideoImageSourceEnabled: boolean = false @@ -101,22 +100,6 @@ struct LiveStreaming { return result == Constants.ErrorCode.ERR_OK } - private updateRemoteUserPriority(isHighPriority: boolean): void { - const engine = this.rtcEngine - if (engine == undefined || this.remoteUid == -1) { - ShowToast.shortToast('Remote user is not available') - return - } - const priority: Constants.UserPriority = isHighPriority ? - Constants.UserPriority.HIGH : Constants.UserPriority.NORANL - const result: number = engine.setRemoteUserPriority(this.remoteUid, priority) - Logger.info(TAG, `setRemoteUserPriority(${this.remoteUid}, ${priority}) result:${result}`) - if (result == Constants.ErrorCode.ERR_OK) { - this.isRemoteUserHighPriority = isHighPriority - } - ShowToast.shortToast(`setRemoteUserPriority: ${this.getResultText(result)}`) - } - private updateVideoImageSource(enabled: boolean, showResult: boolean): boolean { const engine = this.rtcEngine if (engine == undefined || !this.isVideoImageSourceReady || this.videoImageFilePath.length == 0) { @@ -154,14 +137,12 @@ struct LiveStreaming { Logger.info(TAG, "mEventHandler.onUserJoined: " + uid + " , " + collapse); if (this.remoteUid == -1) { this.remoteUid = uid - this.isRemoteUserHighPriority = false } }; config.mEventHandler.onUserOffline = (uid: number, reason: number) => { Logger.info(TAG, "mEventHandler.onUserOffline: " + uid + " , " + reason) if (this.remoteUid == uid) { this.remoteUid = -1 - this.isRemoteUserHighPriority = false } }; config.mEventHandler.onJoinChannelSuccess = (cid: string, uid: number, elapsed: number) => { @@ -174,7 +155,6 @@ struct LiveStreaming { this.isJoin = false this.localUid = -1 this.remoteUid = -1 - this.isRemoteUserHighPriority = false this.isVideoImageSourceEnabled = false } config.mEventHandler.onError = (err: number, message: string) => { @@ -300,26 +280,6 @@ struct LiveStreaming { Divider().margin({ left: 12, right: 12 }) - Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text($r('app.string.remote_user_high_priority')) - .flexGrow(1) - - Toggle({ type: ToggleType.Switch, isOn: this.isRemoteUserHighPriority }) - .enabled(this.remoteUid != -1) - .onChange((isOn: boolean) => { - this.updateRemoteUserPriority(isOn) - }) - .flexGrow(0) - } - .padding({ - top: 8, - bottom: 8, - left: 12, - right: 12 - }) - - Divider().margin({ left: 12, right: 12 }) - Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Text($r('app.string.video_image_source')) .flexGrow(1) @@ -340,11 +300,12 @@ struct LiveStreaming { } .width('100%') } - .layoutWeight(1) .width('100%') .scrollBar(BarState.Auto) - }.width('100%').height('100%') + } + .width('100%') + .padding({ bottom: 16 }) } build() { @@ -501,7 +462,7 @@ struct LiveStreaming { } .backgroundColor($r('app.color.background_shallow_grey')) .bindSheet(this.showSetting, this.buildSettingSheet(), { - detents: [SheetSize.MEDIUM, SheetSize.LARGE, 200], + detents: [SheetSize.FIT_CONTENT], backgroundColor: Color.White, blurStyle: BlurStyle.Thick, showClose: false, diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json index e9d038dc1..97d92b14a 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/element/string.json @@ -408,10 +408,6 @@ "name": "fallback_audio_only", "value": "Audio Only" }, - { - "name": "remote_user_high_priority", - "value": "Remote User High Priority" - }, { "name": "video_image_source", "value": "Video Image Source" diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json index 2dc2a3746..e8c52c32b 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/en_US/element/string.json @@ -408,10 +408,6 @@ "name": "fallback_audio_only", "value": "Audio Only" }, - { - "name": "remote_user_high_priority", - "value": "Remote User High Priority" - }, { "name": "video_image_source", "value": "Video Image Source" diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json index eea7479a7..28a221857 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/zh_CN/element/string.json @@ -408,13 +408,9 @@ "name": "fallback_audio_only", "value": "降级到纯音频" }, - { - "name": "remote_user_high_priority", - "value": "远端用户高优先级" - }, { "name": "video_image_source", - "value": "图片视频轨" + "value": "垫片推流" }, { "name": "get_ntp_time", From 99f617147b35e3f576c52c1646d203e666f3ceac Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 30 Jul 2026 10:29:01 +0800 Subject: [PATCH 3/4] fix(harmony): receive video metadata --- .../entry/src/main/cpp/media_metadata.cpp | 97 +++++++++++++++++-- .../entry/src/main/cpp/media_metadata.h | 5 + .../cpp/types/librawdataprocessing/index.d.ts | 2 +- .../ets/pages/advance/MediaMetadataWrap.ets | 13 ++- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.cpp b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.cpp index 6ed5fda68..57aee9be9 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.cpp +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.cpp @@ -6,11 +6,20 @@ #include "media_metadata.h" #include +#include MediaMetadata::MediaMetadata(uintptr_t rtcEngineHandler) - : rtcEngine_(reinterpret_cast(rtcEngineHandler)), env_(nullptr), wrapper_(nullptr) {} - -MediaMetadata::~MediaMetadata() { napi_delete_reference(env_, wrapper_); } + : rtcEngine_(reinterpret_cast(rtcEngineHandler)), env_(nullptr), wrapper_(nullptr), + metadataCallback_(nullptr) {} + +MediaMetadata::~MediaMetadata() { + ReleaseMetadataCallback(); + delete[] pendingData_; + pendingData_ = nullptr; + if (env_ != nullptr && wrapper_ != nullptr) { + napi_delete_reference(env_, wrapper_); + } +} bool MediaMetadata::onReadyToSendMetadata(Metadata &metadata, agora::rtc::VIDEO_SOURCE_TYPE source_type) { if(pendingData_){ @@ -26,7 +35,51 @@ bool MediaMetadata::onReadyToSendMetadata(Metadata &metadata, agora::rtc::VIDEO_ } void MediaMetadata::onMetadataReceived(const Metadata &metadata) { - AG_INFO("MediaMetadata::onMetadataReceived -- data=%{public}s", metadata.buffer); + if (metadata.buffer == nullptr || metadata.size <= 0) { + AG_ERROR("MediaMetadata::onMetadataReceived -- invalid metadata"); + return; + } + + auto *message = new std::string(reinterpret_cast(metadata.buffer), metadata.size); + AG_INFO("MediaMetadata::onMetadataReceived -- data=%{public}s, size=%{public}d", message->c_str(), metadata.size); + napi_status status = napi_closing; + { + std::lock_guard lock(callbackMutex_); + if (metadataCallback_ != nullptr) { + status = napi_call_threadsafe_function(metadataCallback_, message, napi_tsfn_nonblocking); + } + } + if (status != napi_ok) { + delete message; + } +} + +void MediaMetadata::CallMetadataCallback(napi_env env, napi_value callback, + [[maybe_unused]] void *context, void *data) { + auto *message = static_cast(data); + if (message == nullptr) { + return; + } + if (env == nullptr || callback == nullptr) { + delete message; + return; + } + + napi_value value; + if (napi_create_string_utf8(env, message->data(), message->size(), &value) == napi_ok) { + napi_value undefined; + napi_get_undefined(env, &undefined); + napi_call_function(env, undefined, callback, 1, &value, nullptr); + } + delete message; +} + +void MediaMetadata::ReleaseMetadataCallback() { + std::lock_guard lock(callbackMutex_); + if (metadataCallback_ != nullptr) { + napi_release_threadsafe_function(metadataCallback_, napi_tsfn_abort); + metadataCallback_ = nullptr; + } } void MediaMetadata::Destructor(napi_env env, void *nativeObject, [[maybe_unused]] void *finalize_hint) { @@ -95,8 +148,8 @@ napi_value MediaMetadata::New(napi_env env, napi_callback_info info) { napi_value MediaMetadata::Enable(napi_env env, napi_callback_info info) { AG_INFO("MediaMetadata::Enable called"); - size_t argc = 1; - napi_value args[1]; + size_t argc = 2; + napi_value args[2]; napi_value jsThis; napi_get_cb_info(env, info, &argc, args, &jsThis, nullptr); bool enable; @@ -109,9 +162,39 @@ napi_value MediaMetadata::Enable(napi_env env, napi_callback_info info) { int ret = -1; if (enable) { - ret = obj->rtcEngine_->registerMediaMetadataObserver(obj, IMetadataObserver::METADATA_TYPE::VIDEO_METADATA); + obj->ReleaseMetadataCallback(); + if (argc >= 2) { + napi_valuetype callbackType = napi_undefined; + if (napi_typeof(env, args[1], &callbackType) != napi_ok || callbackType != napi_function) { + ret = -2; + } else { + napi_value resourceName; + napi_create_string_utf8(env, "MediaMetadataReceived", NAPI_AUTO_LENGTH, &resourceName); + napi_threadsafe_function callback = nullptr; + napi_status status = napi_create_threadsafe_function( + env, args[1], nullptr, resourceName, 0, 1, nullptr, nullptr, nullptr, + CallMetadataCallback, &callback); + if (status == napi_ok) { + { + std::lock_guard lock(obj->callbackMutex_); + obj->metadataCallback_ = callback; + } + ret = obj->rtcEngine_->registerMediaMetadataObserver( + obj, IMetadataObserver::METADATA_TYPE::VIDEO_METADATA); + if (ret != 0) { + obj->ReleaseMetadataCallback(); + } + } else { + ret = -3; + } + } + } else { + ret = obj->rtcEngine_->registerMediaMetadataObserver( + obj, IMetadataObserver::METADATA_TYPE::VIDEO_METADATA); + } } else { ret = obj->rtcEngine_->unregisterMediaMetadataObserver(obj, IMetadataObserver::METADATA_TYPE::VIDEO_METADATA); + obj->ReleaseMetadataCallback(); } napi_value num; diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.h b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.h index 499cbb4a7..1896112b9 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.h +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/media_metadata.h @@ -11,6 +11,7 @@ #include "utils.h" #include "napi/native_api.h" #include "IAgoraRtcEngine.h" +#include static thread_local napi_ref g_media_metadata_ref = nullptr; @@ -27,6 +28,8 @@ class MediaMetadata : public agora::rtc::IMetadataObserver { static napi_value New(napi_env env, napi_callback_info info); static napi_value Enable(napi_env env, napi_callback_info info); static napi_value Send(napi_env env, napi_callback_info info); + static void CallMetadataCallback(napi_env env, napi_value callback, void *context, void *data); + void ReleaseMetadataCallback(); // IMetadataObserver int getMaxMetadataSize() override { return DEFAULT_METADATA_SIZE_IN_BYTE; } @@ -38,5 +41,7 @@ class MediaMetadata : public agora::rtc::IMetadataObserver { napi_ref wrapper_; unsigned char* pendingData_ = nullptr; int pendingDataSize_ = 0; + std::mutex callbackMutex_; + napi_threadsafe_function metadataCallback_; }; #endif // APIEXAMPLE_MEDIA_METADATA_H diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts index d793e3202..4e5d9e0e3 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/cpp/types/librawdataprocessing/index.d.ts @@ -24,6 +24,6 @@ export interface SnapshotFrame { export class MediaMetadata { constructor(arg: bigint); - enable: (enable:boolean) => number; + enable: (enable:boolean, onMetadataReceived?: (data: string) => void) => number; send: (data: string) => number; } diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/MediaMetadataWrap.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/MediaMetadataWrap.ets index 30992e746..43a8bf9fd 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/MediaMetadataWrap.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/MediaMetadataWrap.ets @@ -72,10 +72,19 @@ struct MediaMetadataWrap { this.rtcEngine.enableVideo() this.native = new MediaMetadata(this.rtcEngine.getNativeHandle()); - this.native.enable(true) + const result: number = this.native.enable(true, (data: string) => { + Logger.info(TAG, `onMetadataReceived: ${data}`) + ShowToast.longToast(`Metadata received: ${data}`) + }) + Logger.info(TAG, `enable media metadata result: ${result}`) + if (result != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`enable media metadata failed: ${result}`) + } } aboutToDisappear(): void { + this.native?.enable(false) + this.native = undefined if (this.rtcEngine != undefined) { if (this.isJoin) { this.rtcEngine.stopPreview(); @@ -222,4 +231,4 @@ struct MediaMetadataWrap { } .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} From d8da3ee9e9a073a4b9b12a82530f13d079651611 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 30 Jul 2026 11:45:28 +0800 Subject: [PATCH 4/4] fix(harmony): stabilize local video transcoding --- .../pages/advance/LocalVideoTranscoding.ets | 232 +++++++++++++----- 1 file changed, 168 insertions(+), 64 deletions(-) diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LocalVideoTranscoding.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LocalVideoTranscoding.ets index 184618613..6e5cc0101 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LocalVideoTranscoding.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/LocalVideoTranscoding.ets @@ -2,10 +2,12 @@ import { CameraCapturerConfiguration, ChannelMediaOptions, Constants, + IMediaPlayer, + IMediaPlayerObserver, LocalTranscoderConfiguration, + MediaPlayerSource, RtcEngine, RtcEngineConfig, - ScreenCaptureParameters, TranscodingVideoStream, VideoCanvas, VideoDimensions, @@ -21,16 +23,28 @@ import { GlobalInfo } from '../Data'; import { JSON } from '@kit.ArkTS'; const TAG: string = 'LocalVideoTranscoding' +const MEDIA_URL: string = 'https://agora-adc-artifacts.s3.cn-north-1.amazonaws.com.cn/resources/sample.mp4' @Entry @Component struct LocalVideoTranscoding { private rtcEngine: RtcEngine | undefined = undefined; + private mediaPlayer: IMediaPlayer | undefined = undefined; + private mediaPlayerObserver: IMediaPlayerObserver = { + onPlayerStateChanged: (state: Constants.MediaPlayerState, reason: Constants.MediaPlayerReason) => { + Logger.info(TAG, "onPlayerStateChanged state: " + state + " reason: " + reason) + if (state == Constants.MediaPlayerState.FAILED) { + ShowToast.longToast("Media player failed: " + reason) + } + } + } @State message: string = 'Hello World'; @State channelName: string = '' @State isJoin: boolean = false + @State isOperating: boolean = false @State localUid: number = -1 @State remoteUid: number = -1 + private isTranscodingStarted: boolean = false aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear') @@ -53,16 +67,19 @@ struct LocalVideoTranscoding { config.mEventHandler.onJoinChannelSuccess = (cid: string, uid: number, elapsed: number) => { Logger.info(TAG, "mEventHandler.onJoinChannelSuccess: " + uid); this.isJoin = true + this.isOperating = false this.localUid = uid } config.mEventHandler.onLeaveChannel = () => { Logger.info(TAG, "mEventHandler.onLeaveChannel"); this.isJoin = false + this.isOperating = false this.localUid = -1 this.remoteUid = -1 } config.mEventHandler.onError = (err: number, message: string) => { Logger.info(TAG, "mEventHandler.onError: " + err + " message " + message); + this.isOperating = false ShowToast.longToast("onError " + err + " : " + message) } config.mEventHandler.onLocalVideoTranscoderError = (stream:TranscodingVideoStream, error:Constants.VideoTranscoderError) => { @@ -88,26 +105,41 @@ struct LocalVideoTranscoding { this.rtcEngine.setParameters("{\"che.video.hmos_encoder.surface_enable\": true}"); //设置编码是否走texture模式 this.rtcEngine.setParameters("{\"engine.video.enable_hw_encoder\": true}"); //设置true走硬编,需要强制设置 - this.rtcEngine.setParameters("{\"che.video.hmos_screen.surface_enable\": true}"); //设置屏幕共享采集纹理 - this.rtcEngine.setParameters("{\"he.video.hmos_screen.copy_enable\": true}"); //设置屏幕共享采集纹理 - let encoderConfig = new VideoEncoderConfiguration() encoderConfig.dimensions = GlobalInfo.settings.dimensions encoderConfig.frameRate = GlobalInfo.settings.frameRate encoderConfig.orientationMode = GlobalInfo.settings.orientationMode this.rtcEngine.setVideoEncoderConfiguration(encoderConfig) this.rtcEngine.enableVideo() + + this.mediaPlayer = this.rtcEngine.createMediaPlayer() + if (this.mediaPlayer == undefined) { + ShowToast.longToast("createMediaPlayer failed") + return + } + this.mediaPlayer.registerPlayerObserver(this.mediaPlayerObserver) + this.mediaPlayer.setLoopCount(-1) + this.mediaPlayer.adjustPlayoutVolume(0) + + let mediaSource = new MediaPlayerSource() + mediaSource.url = MEDIA_URL + mediaSource.autoPlay = false + let ret = this.mediaPlayer.openWithMediaSource(mediaSource) + if (ret != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast("openWithMediaSource error " + ret) + } } aboutToDisappear(): void { if (this.rtcEngine != undefined) { + this.stopTranscoding() if (this.isJoin) { - this.rtcEngine?.stopLocalVideoTranscoder() - this.rtcEngine?.stopCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY) - this.rtcEngine?.stopScreenCapture() - this.rtcEngine.stopPreview(); this.rtcEngine.leaveChannel(); } + this.mediaPlayer?.stop() + this.mediaPlayer?.unRegisterPlayerObserver() + this.mediaPlayer?.destroy() + this.mediaPlayer = undefined Logger.info(TAG, "destroy begin") RtcEngine.destroy().then(() => { Logger.info(TAG, "destroy done") @@ -117,6 +149,82 @@ struct LocalVideoTranscoding { } } + private stopTranscoding(): void { + this.rtcEngine?.stopPreview(Constants.VideoSourceType.TRANSCODED) + if (this.isTranscodingStarted) { + this.rtcEngine?.stopLocalVideoTranscoder() + this.rtcEngine?.stopCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY) + this.mediaPlayer?.pause() + this.isTranscodingStarted = false + } + } + + private startTranscoding(): boolean { + if (this.rtcEngine == undefined || this.mediaPlayer == undefined) { + ShowToast.longToast("RTC engine or media player is not ready") + return false + } + + if (!this.isTranscodingStarted) { + let width = GlobalInfo.settings.dimensions.width + let height = GlobalInfo.settings.dimensions.height + + let cameraConfig = new CameraCapturerConfiguration() + let cameraRet = this.rtcEngine.startCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY, cameraConfig) + if (cameraRet != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast("startCameraCapture error " + cameraRet) + return false + } + + let config = new LocalTranscoderConfiguration() + config.transcodingVideoStreams = new Array(2) + config.videoOutputConfiguration.dimensions = new VideoDimensions(width,height) + config.transcodingVideoStreams[0] = new TranscodingVideoStream() + config.transcodingVideoStreams[0].sourceType = Constants.VideoSourceType.MEDIA_PLAYER + config.transcodingVideoStreams[0].mediaPlayerId = this.mediaPlayer.getMediaPlayerId() + config.transcodingVideoStreams[0].width = width + config.transcodingVideoStreams[0].height = height + config.transcodingVideoStreams[0].x = 0 + config.transcodingVideoStreams[0].y = 0 + config.transcodingVideoStreams[0].zOrder = 1 + + config.transcodingVideoStreams[1] = new TranscodingVideoStream() + config.transcodingVideoStreams[1].sourceType = Constants.VideoSourceType.CAMERA_PRIMARY + config.transcodingVideoStreams[1].width = width/2 + config.transcodingVideoStreams[1].height = height/2 + config.transcodingVideoStreams[1].x = 0 + config.transcodingVideoStreams[1].y = height / 2 + config.transcodingVideoStreams[1].zOrder = 2 + config.transcodingVideoStreams[1].mirror = true + + let playerRet = this.mediaPlayer.play() + if (playerRet != Constants.ErrorCode.ERR_OK) { + this.rtcEngine.stopCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY) + ShowToast.longToast("mediaPlayer.play error " + playerRet) + return false + } + + let transcoderRet = this.rtcEngine.startLocalVideoTranscoder(config) + if (transcoderRet != Constants.ErrorCode.ERR_OK) { + this.mediaPlayer.pause() + this.rtcEngine.stopCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY) + ShowToast.longToast("startLocalVideoTranscoder error " + transcoderRet) + return false + } + + let previewRet = this.rtcEngine.startPreview(Constants.VideoSourceType.TRANSCODED) + if (previewRet != Constants.ErrorCode.ERR_OK) { + this.rtcEngine.stopLocalVideoTranscoder() + this.rtcEngine.stopCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY) + this.mediaPlayer.pause() + ShowToast.longToast("startPreview error " + previewRet) + return false + } + this.isTranscodingStarted = true + } + return true + } + build() { Column() { TitleBar({ @@ -125,20 +233,18 @@ struct LocalVideoTranscoding { }) Stack({ alignContent: Alignment.TopEnd }) { - if (this.localUid != -1) { - XComponent({ - id: 'preview_local', - type: XComponentType.SURFACE, - libraryname: Constants.AGORA_LIB_NAME, - }).onLoad(() => { - let localCanvas = new VideoCanvas("preview_local"); - localCanvas.uid = this.localUid; - localCanvas.renderMode = VideoCanvas.RENDER_MODE_FIT; - localCanvas.mirrorMode = VideoCanvas.VIDEO_MIRROR_MODE_DISABLED; - localCanvas.sourceType = Constants.VideoSourceType.TRANSCODED - this.rtcEngine?.setupLocalVideo(localCanvas); - }).width('100%').height('100%') - } + XComponent({ + id: 'preview_local', + type: XComponentType.SURFACE, + libraryname: Constants.AGORA_LIB_NAME, + }).onLoad(() => { + let localCanvas = new VideoCanvas("preview_local"); + localCanvas.uid = 0; + localCanvas.renderMode = VideoCanvas.RENDER_MODE_FIT; + localCanvas.mirrorMode = VideoCanvas.VIDEO_MIRROR_MODE_DISABLED; + localCanvas.sourceType = Constants.VideoSourceType.TRANSCODED + this.rtcEngine?.setupLocalVideo(localCanvas); + }).width('100%').height('100%') if (this.remoteUid != -1) { XComponent({ @@ -154,13 +260,21 @@ struct LocalVideoTranscoding { this.rtcEngine?.setupRemoteVideo(localCanvas); }).width('30%').height('30%').margin({ top: 12, right: 12 }) } + + if (!this.isJoin) { + Column() { + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_shallow_grey')) + } } .layoutWeight(1) Row() { TextInput({ placeholder: $r('app.string.text_input_channel_name') }) .id("input_channel_name") - .enabled(!this.isJoin) + .enabled(!this.isJoin && !this.isOperating) .enableKeyboardOnFocus(false) .onChange((value: string) => { this.channelName = value @@ -170,55 +284,37 @@ struct LocalVideoTranscoding { Button(this.isJoin ? $r('app.string.leave') : $r('app.string.join')) .id('button_to_join') + .enabled(!this.isOperating) .onClick(async () => { focusControl.requestFocus("button_to_join") + if (this.isOperating) { + return + } if (!this.isJoin) { if (this.channelName == "") { ShowToast.shortToast("The channel name is empty!") return } + this.isOperating = true if (!(await PermissionHelper.checkPermissions( getContext(this) as common.UIAbilityContext, ['ohos.permission.MICROPHONE', 'ohos.permission.CAMERA'] ))) { + this.isOperating = false ShowToast.shortToast("Permission leak!") return } - - let width = GlobalInfo.settings.dimensions.height - let height = GlobalInfo.settings.dimensions.width - - let screenConfig = new ScreenCaptureParameters() - screenConfig.videoParams.dimensions = new VideoDimensions(width,height) - this.rtcEngine?.startScreenCapture(screenConfig) - - let cameraConfig = new CameraCapturerConfiguration() - this.rtcEngine?.startCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY, cameraConfig) - - let config = new LocalTranscoderConfiguration() - config.transcodingVideoStreams = new Array(2) - config.videoOutputConfiguration.dimensions = new VideoDimensions(width,height) - config.transcodingVideoStreams[0] = new TranscodingVideoStream(); - config.transcodingVideoStreams[0].sourceType = Constants.VideoSourceType.SCREEN_PRIMARY - config.transcodingVideoStreams[0].width = width - config.transcodingVideoStreams[0].height = height - config.transcodingVideoStreams[0].x = 0 - config.transcodingVideoStreams[0].y = 0 - config.transcodingVideoStreams[0].zOrder = 1 - - config.transcodingVideoStreams[1] = new TranscodingVideoStream() - config.transcodingVideoStreams[1].sourceType = Constants.VideoSourceType.CAMERA_PRIMARY - config.transcodingVideoStreams[1].width = width/2 - config.transcodingVideoStreams[1].height = height/2 - config.transcodingVideoStreams[1].x = 0 - config.transcodingVideoStreams[1].y = height / 2 - config.transcodingVideoStreams[1].zOrder = 2 - config.transcodingVideoStreams[1].mirror = true - - this.rtcEngine?.startLocalVideoTranscoder(config) - - this.rtcEngine?.startPreview(Constants.VideoSourceType.TRANSCODED) + let rtcEngine = this.rtcEngine + if (rtcEngine == undefined) { + this.isOperating = false + ShowToast.longToast("RTC engine is not ready") + return + } + if (!this.startTranscoding()) { + this.isOperating = false + return + } let mediaOption: ChannelMediaOptions = new ChannelMediaOptions(); mediaOption.publishCameraTrack = false; @@ -230,18 +326,26 @@ struct LocalVideoTranscoding { mediaOption.clientRoleType = Constants.ClientRole.BROADCASTER; let token = "" - let ret = this.rtcEngine?.joinChannelWithOptions(token, this.channelName, 0, mediaOption) + let ret = rtcEngine.joinChannelWithOptions(token, this.channelName, 0, mediaOption) if (ret != Constants.ErrorCode.ERR_OK) { + this.isOperating = false ShowToast.longToast("joinChannelWithOptions error " + ret + " : " + RtcEngine.getErrorDescription(ret)) return } } else { - this.rtcEngine?.stopLocalVideoTranscoder() - this.rtcEngine?.stopCameraCapture(Constants.VideoSourceType.CAMERA_PRIMARY) - this.rtcEngine?.stopScreenCapture() - this.rtcEngine?.stopPreview() - this.rtcEngine?.leaveChannel() + this.isOperating = true + let rtcEngine = this.rtcEngine + if (rtcEngine == undefined) { + this.isOperating = false + ShowToast.longToast("RTC engine is not ready") + return + } + let ret = rtcEngine.leaveChannel() + if (ret != Constants.ErrorCode.ERR_OK) { + this.isOperating = false + ShowToast.longToast("leaveChannel error " + ret) + } } }) } @@ -254,4 +358,4 @@ struct LocalVideoTranscoding { } .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +}