diff --git a/.github/ci/build/build_hmos.groovy b/.github/ci/build/build_hmos.groovy new file mode 100644 index 000000000..e46f7bb8a --- /dev/null +++ b/.github/ci/build/build_hmos.groovy @@ -0,0 +1,63 @@ +// -*- mode: groovy -*- +// vim: set filetype=groovy : +@Library('agora-build-pipeline-library') _ + +buildUtils = new agora.build.BuildUtils() + +compileConfig = [ + "sourceDir": "api-examples", + "non-publish": [ + "command": "./.github/ci/build/build_hmos.sh", + "extraArgs": "", + ], + "publish": [ + "command": "./.github/ci/build/build_hmos.sh", + "extraArgs": "", + ] +] + +def doBuild(buildVariables) { + type = params.Package_Publish ? "publish" : "non-publish" + command = compileConfig.get(type).command + preCommand = compileConfig.get(type).get("preCommand", "") + postCommand = compileConfig.get(type).get("postCommand", "") + extraArgs = compileConfig.get(type).extraArgs + extraArgs += " " + params.getOrDefault("extra_args", "") + commandConfig = [ + "command": command, + "sourceRoot": "${compileConfig.sourceDir}", + "extraArgs": extraArgs + ] + loadResources(["config.json", "artifactory_utils.py"]) + buildUtils.customBuild(commandConfig, preCommand, postCommand) +} + +def doPublish(buildVariables) { + if (!params.Package_Publish) { + return + } + + (shortVersion, releaseVersion) = buildUtils.getBranchVersion() + def archiveInfos = [ + [ + "type": "ARTIFACTORY", + "archivePattern": "*.zip", + "serverPath": "ApiExample/${shortVersion}/${buildVariables.buildDate}/${env.platform}", + "serverRepo": "SDK_repo" + ], + [ + "type": "ARTIFACTORY", + "archivePattern": "*.hap", + "serverPath": "ApiExample/${shortVersion}/${buildVariables.buildDate}/${env.platform}", + "serverRepo": "SDK_repo" + ] + ] + def archiveUrls = (archive.archiveFiles(archiveInfos) ?: []) as Set + if (archiveUrls) { + writeFile(file: "package_urls", text: archiveUrls.join("\n"), encoding: "utf-8") + } + archiveArtifacts(artifacts: "package_urls", allowEmptyArchive: true) + sh "rm -f -- *.zip *.hap" +} + +pipelineLoad(this, "ApiExample", "build", "harmonyos", "RTC-Sample") diff --git a/.github/ci/build/build_hmos.sh b/.github/ci/build/build_hmos.sh new file mode 100755 index 000000000..3665a6ae3 --- /dev/null +++ b/.github/ci/build/build_hmos.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +ci_dir="$(cd "$(dirname "$0")" && pwd)" +repo_root="$(cd "${ci_dir}/../../.." && pwd)" +project_dir="${repo_root}/HarmonyOS_NEXT/APIExample" +libs_dir="${project_dir}/entry/libs" +output_dir="${WORKSPACE:-${repo_root}}" + +if [[ -z "${sdk_url:-}" ]]; then + echo "[ERROR] sdk_url is required" + exit 1 +fi + +sdk_file_name="$(basename "${sdk_url%%\?*}")" +if [[ "${sdk_file_name}" != *.zip ]]; then + echo "[ERROR] sdk_url must point to a FULL SDK .zip file: ${sdk_file_name}" + exit 1 +fi + +stage_dir="$(mktemp -d "${TMPDIR:-/tmp}/apiexample-hmos.XXXXXX")" +trap 'rm -rf "${stage_dir}"' EXIT + +sdk_archive="${stage_dir}/${sdk_file_name}" +sdk_extract_dir="${stage_dir}/sdk" +mkdir -p "${sdk_extract_dir}" +curl -fL --retry 3 --output "${sdk_archive}" "${sdk_url}" +unzip -q "${sdk_archive}" -d "${sdk_extract_dir}" + +sdk_hars=() +while IFS= read -r sdk_har; do + sdk_hars+=("${sdk_har}") +done < <(find "${sdk_extract_dir}" -type f -path '*/rtc/sdk/AgoraRtcSdk.har') + +if [[ "${#sdk_hars[@]}" -ne 1 ]]; then + echo "[ERROR] Expected exactly one rtc/sdk/AgoraRtcSdk.har in ${sdk_file_name}, found ${#sdk_hars[@]}" + exit 1 +fi + +sdk_har="${sdk_hars[0]}" +sdk_root="${sdk_har%/rtc/sdk/AgoraRtcSdk.har}" +sdk_name="$(basename "${sdk_root}")" + +mkdir -p "${libs_dir}" +cp -f "${sdk_har}" "${libs_dir}/AgoraRtcSdk.har" + +package_metadata="${stage_dir}/oh-package.json5" +tar -xOzf "${sdk_har}" package/oh-package.json5 > "${package_metadata}" +detected_version="$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["version"])' "${package_metadata}")" +export SDK_VERSION="${SDK_VERSION:-${detected_version}}" + +sample_dir="${sdk_root}/rtc/samples/API-Example" +rm -rf "${sample_dir}" +mkdir -p "${sample_dir}" +rsync -a \ + --exclude '.hvigor/' \ + --exclude 'build/' \ + --exclude 'oh_modules/' \ + "${project_dir}/" "${sample_dir}/" + +build_number="${BUILD_NUMBER:-local}" +timestamp="$(date '+%Y%m%d%H%M%S')" +export ARTIFACT_TIMESTAMP="${timestamp}" +zip_file="${repo_root}/APIExample_${build_number}_${SDK_VERSION}_${timestamp}.zip" +( + cd "$(dirname "${sdk_root}")" + zip -qry "${zip_file}" "${sdk_name}" +) + +if [[ "${compile_project:-true}" == "true" ]]; then + "${project_dir}/cloud_build.sh" + + hap_file="${project_dir}/APIExample_${build_number}_${SDK_VERSION}_${timestamp}.hap" + if [[ ! -s "${hap_file}" ]]; then + echo "[ERROR] No signed HAP was produced" + exit 1 + fi + cp -f "${hap_file}" "${output_dir}/" +fi + +if [[ "${output_dir}" != "${repo_root}" ]]; then + cp -f "${zip_file}" "${output_dir}/" +fi + +echo "[INFO] HarmonyOS artifacts are ready in ${output_dir}" diff --git a/HarmonyOS_NEXT/APIExample/cloud_build.sh b/HarmonyOS_NEXT/APIExample/cloud_build.sh new file mode 100755 index 000000000..8b963f360 --- /dev/null +++ b/HarmonyOS_NEXT/APIExample/cloud_build.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 + +project_dir="$(cd "$(dirname "$0")" && pwd)" +deveco_home="${DEVECO_HOME:-/Applications/DevEco-Studio.app/Contents}" +ohpm_bin="${OHPM_BIN:-${deveco_home}/tools/ohpm/bin/ohpm}" +hvigor_bin="${HVIGOR_BIN:-${deveco_home}/tools/hvigor/bin/hvigorw}" +java_bin="${JAVA_BIN:-${deveco_home}/jbr/Contents/Home/bin/java}" +node_home="${HMOS_NODE_HOME:-${deveco_home}/tools/node}" +node_bin="${node_home}/bin/node" +deveco_sdk_home="${deveco_home}/sdk" +sign_tool="${HMOS_SIGN_TOOL_JAR:-${deveco_home}/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar}" + +signing_dir="${HMOS_SIGN_CONFIG_DIR:-/Users/admin/.ohos/config/apiexample-config}" +profile_file="${HMOS_PROFILE_FILE:-${signing_dir}/debugDebug.p7b}" +keystore_file="${HMOS_KEYSTORE_FILE:-${signing_dir}/wayang.p12}" +certificate_file="${HMOS_CERTIFICATE_FILE:-${signing_dir}/wayangAgora.cer}" + +require_file() { + if [[ ! -f "$1" ]]; then + echo "[ERROR] Required file not found: $1" + exit 1 + fi +} + +require_executable() { + if [[ ! -x "$1" ]]; then + echo "[ERROR] Required executable not found: $1" + exit 1 + fi +} + +require_directory() { + if [[ ! -d "$1" ]]; then + echo "[ERROR] Required directory not found: $1" + exit 1 + fi +} + +if [[ -z "${APP_ID:-}" ]]; then + echo "[ERROR] APP_ID is required" + exit 1 +fi +if [[ -z "${HMOS_KEY_PWD:-}" ]]; then + echo "[ERROR] HMOS_KEY_PWD is required" + exit 1 +fi + +require_executable "${ohpm_bin}" +require_executable "${hvigor_bin}" +require_executable "${java_bin}" +require_executable "${node_bin}" +require_directory "${deveco_sdk_home}" +require_file "${sign_tool}" +require_file "${profile_file}" +require_file "${keystore_file}" +require_file "${certificate_file}" + +export NODE_HOME="${node_home}" +export PATH="${node_home}/bin:${PATH}" +export DEVECO_SDK_HOME="${deveco_sdk_home}" + +app_id="${APP_ID}" +app_id="${app_id#\'}" +app_id="${app_id%\'}" +app_id="${app_id#\"}" +app_id="${app_id%\"}" +if [[ ! "${app_id}" =~ ^[[:xdigit:]]{32}$ ]]; then + echo "[ERROR] APP_ID must be a 32-character hexadecimal value" + exit 1 +fi +escaped_app_id="${app_id//&/\\&}" + +key_center="${project_dir}/entry/src/main/ets/common/KeyCenter.ets" +sed -E -i '' \ + "s#^export const AppID: string[[:space:]]*=.*\$#export const AppID: string = '${escaped_app_id}'#" \ + "${key_center}" +sed -E -i '' \ + "s#^export const AppCertificate: string[[:space:]]*=.*\$#export const AppCertificate: string = ''#" \ + "${key_center}" + +"${java_bin}" -version +( + cd "${project_dir}" + "${ohpm_bin}" install + cd "${project_dir}/entry" + "${ohpm_bin}" install + cd "${project_dir}" + "${hvigor_bin}" clean --no-daemon + "${hvigor_bin}" assembleHap --mode module -p product=default -p buildMode=debug --no-daemon +) + +unsigned_hap="${project_dir}/entry/build/default/outputs/default/entry-default-unsigned.hap" +require_file "${unsigned_hap}" + +build_number="${BUILD_NUMBER:-local}" +sdk_version="${SDK_VERSION:-unknown}" +artifact_timestamp="${ARTIFACT_TIMESTAMP:-$(date '+%Y%m%d%H%M%S')}" +signed_hap="${project_dir}/APIExample_${build_number}_${sdk_version}_${artifact_timestamp}.hap" + +"${java_bin}" -jar "${sign_tool}" sign-app \ + -keyAlias "${HMOS_KEY_PWD}" \ + -signAlg "SHA256withECDSA" \ + -mode "localSign" \ + -appCertFile "${certificate_file}" \ + -profileFile "${profile_file}" \ + -keystoreFile "${keystore_file}" \ + -inFile "${unsigned_hap}" \ + -outFile "${signed_hap}" \ + -keyPwd "${HMOS_KEY_PWD}" \ + -keystorePwd "${HMOS_KEY_PWD}" \ + -signCode "1" + +if [[ ! -s "${signed_hap}" ]]; then + echo "[ERROR] Signed HAP was not produced" + exit 1 +fi + +echo "[INFO] Signed HAP: ${signed_hap}" diff --git a/HarmonyOS_NEXT/APIExample/entry/oh-package-lock.json5 b/HarmonyOS_NEXT/APIExample/entry/oh-package-lock.json5 index 60fc2b12c..9ca3492d0 100644 --- a/HarmonyOS_NEXT/APIExample/entry/oh-package-lock.json5 +++ b/HarmonyOS_NEXT/APIExample/entry/oh-package-lock.json5 @@ -1,28 +1,29 @@ { "meta": { - "stableOrder": true + "stableOrder": true, + "enableUnifiedLockfile": false }, "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { - "AgoraRtcSdk@libs/AgoraRtcSdk.har": "AgoraRtcSdk@libs/AgoraRtcSdk.har", - "libagora_rtc_sdk.so@../oh_modules/.ohpm/AgoraRtcSdk@psqmdffzbvli66vvuhpgemb+uuiby9okskr7kzozry4=/oh_modules/AgoraRtcSdk/src/main/cpp/types/libagora_rtc_sdk": "libagora_rtc_sdk.so@../oh_modules/.ohpm/AgoraRtcSdk@psqmdffzbvli66vvuhpgemb+uuiby9okskr7kzozry4=/oh_modules/AgoraRtcSdk/src/main/cpp/types/libagora_rtc_sdk", + "@shengwang/rtc-full@libs/AgoraRtcSdk.har": "@shengwang/rtc-full@libs/AgoraRtcSdk.har", + "libagora_rtc_sdk.so@../oh_modules/.ohpm/@shengwang+rtc-full@7auvsomqdq0xh3w+groigawxirtei0blesxqhfnekrs=/oh_modules/@shengwang/rtc-full/src/main/cpp/types/libagora_rtc_sdk": "libagora_rtc_sdk.so@../oh_modules/.ohpm/@shengwang+rtc-full@7auvsomqdq0xh3w+groigawxirtei0blesxqhfnekrs=/oh_modules/@shengwang/rtc-full/src/main/cpp/types/libagora_rtc_sdk", "librawdataprocessing.so@src/main/cpp/types/librawdataprocessing": "librawdataprocessing.so@src/main/cpp/types/librawdataprocessing" }, "packages": { - "AgoraRtcSdk@libs/AgoraRtcSdk.har": { - "name": "AgoraRtcSdk", - "version": "v4.3.0-beta.1", + "@shengwang/rtc-full@libs/AgoraRtcSdk.har": { + "name": "@shengwang/rtc-full", + "version": "4.6.3", "resolved": "libs/AgoraRtcSdk.har", "registryType": "local", "dependencies": { "libagora_rtc_sdk.so": "file:./src/main/cpp/types/libagora_rtc_sdk" } }, - "libagora_rtc_sdk.so@../oh_modules/.ohpm/AgoraRtcSdk@psqmdffzbvli66vvuhpgemb+uuiby9okskr7kzozry4=/oh_modules/AgoraRtcSdk/src/main/cpp/types/libagora_rtc_sdk": { + "libagora_rtc_sdk.so@../oh_modules/.ohpm/@shengwang+rtc-full@7auvsomqdq0xh3w+groigawxirtei0blesxqhfnekrs=/oh_modules/@shengwang/rtc-full/src/main/cpp/types/libagora_rtc_sdk": { "name": "libagora_rtc_sdk.so", "version": "0.0.0", - "resolved": "../oh_modules/.ohpm/AgoraRtcSdk@psqmdffzbvli66vvuhpgemb+uuiby9okskr7kzozry4=/oh_modules/AgoraRtcSdk/src/main/cpp/types/libagora_rtc_sdk", + "resolved": "../oh_modules/.ohpm/@shengwang+rtc-full@7auvsomqdq0xh3w+groigawxirtei0blesxqhfnekrs=/oh_modules/@shengwang/rtc-full/src/main/cpp/types/libagora_rtc_sdk", "registryType": "local" }, "librawdataprocessing.so@src/main/cpp/types/librawdataprocessing": { diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/Data.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/Data.ets index 682eec7a3..2ac17f178 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/Data.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/Data.ets @@ -31,10 +31,22 @@ export const ADVANCE_ITEMS: ExampleItem[] = title: $r('app.string.item_joinmultichannel'), url: 'pages/advance/JoinMultiChannel' }, + { + title: $r('app.string.item_useraccount'), + url: 'pages/advance/UserAccount' + }, { title: $r('app.string.item_mediametadata'), url: 'pages/advance/MediaMetadataWrap' }, + { + title: $r('app.string.item_voiceeffects'), + url: 'pages/advance/VoiceEffects' + }, + { + title: $r('app.string.item_videobeauty'), + url: 'pages/advance/VideoBeauty' + }, { title: $r('app.string.item_customaudiosource'), url: 'pages/advance/CustomAudioSource' @@ -103,4 +115,4 @@ export class GlobalInfo { frameRate: 15, orientationMode: OrientationMode.ORIENTATION_MODE_ADAPTIVE } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/CustomVideoSource.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/CustomVideoSource.ets index 1ab4a38a8..03d641305 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/CustomVideoSource.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/CustomVideoSource.ets @@ -123,7 +123,7 @@ struct CustomVideoSource { VideoPixelFormat.VIDEO_PIXEL_I420); frame.buffer = frameBuffer.buffer as ArrayBuffer; frame.rotation = 0; - frame.yStride = width; + frame.stride = width; frame.height = height; frame.timestamp = 0; let ret = this.rtcEngine?.pushExternalVideoFrame(frame) @@ -248,4 +248,4 @@ struct CustomVideoSource { } .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} 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 b1b0f65b3..6c636e925 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 @@ -2,6 +2,7 @@ import { ChannelMediaOptions, ClientRoleOptions, Constants, + ImageTrackOptions, RtcEngine, RtcEngineConfig, VideoCanvas, @@ -16,13 +17,28 @@ import { common } from '@kit.AbilityKit'; import { GlobalInfo } from '../Data'; import { TokenUtils } from '../../util/TokenUtils'; import { LengthMetrics } from '@kit.ArkUI'; +import { fileIo } from '@kit.CoreFileKit'; +import { BusinessError } from '@kit.BasicServicesKit'; const TAG: string = 'LiveStreaming' +const VIDEO_IMAGE_FILE_NAME: string = 'bg_blue.png' +const VIDEO_IMAGE_FPS: number = 15 @Entry @Component struct LiveStreaming { private rtcEngine: RtcEngine | undefined = undefined; + private videoImageFilePath: string = '' + private remoteFallbackOptions: Constants.StreamFallbackOptions[] = [ + Constants.StreamFallbackOptions.DISABLED, + Constants.StreamFallbackOptions.VIDEO_STREAM_LOW, + Constants.StreamFallbackOptions.AUDIO_ONLY + ] + private remoteFallbackSelects: ResourceStr[] = [ + $r('app.string.fallback_disabled'), + $r('app.string.fallback_low_stream'), + $r('app.string.fallback_audio_only') + ] @State message: string = 'Hello World'; @State channelName: string = '' @State isJoin: boolean = false @@ -32,6 +48,101 @@ struct LiveStreaming { @State showSetting: boolean = false @State lowLegacyLiving: boolean = false @State pullLowStream: boolean = false + @State remoteFallbackIndex: number = 1 + @State isRemoteUserHighPriority: boolean = false + @State isVideoImageSourceReady: boolean = false + @State isVideoImageSourceEnabled: boolean = false + + private getResultText(result: number): string { + return result == Constants.ErrorCode.ERR_OK ? 'success' : `error(${result})` + } + + private prepareVideoImageFile(context: common.UIAbilityContext): void { + const targetPath: string = `${context.filesDir}/${VIDEO_IMAGE_FILE_NAME}` + context.resourceManager.getRawFileContent(VIDEO_IMAGE_FILE_NAME) + .then((buffer: Uint8Array) => { + const file = fileIo.openSync(targetPath, + fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.TRUNC) + let writtenLength: number = 0 + try { + writtenLength = fileIo.writeSync(file.fd, buffer.buffer as ArrayBuffer) + } finally { + fileIo.closeSync(file) + } + if (writtenLength != buffer.byteLength) { + this.videoImageFilePath = '' + this.isVideoImageSourceReady = false + Logger.error(TAG, `prepareVideoImageFile incomplete:${writtenLength}/${buffer.byteLength}`) + ShowToast.longToast('prepareVideoImageFile: incomplete write') + return + } + this.videoImageFilePath = targetPath + this.isVideoImageSourceReady = true + Logger.info(TAG, `prepareVideoImageFile success:${targetPath}`) + }) + .catch((error: BusinessError) => { + this.videoImageFilePath = '' + this.isVideoImageSourceReady = false + Logger.error(TAG, `prepareVideoImageFile failed. Code:${error.code}, message:${error.message}`) + ShowToast.longToast(`prepareVideoImageFile: error(${error.code})`) + }) + } + + private applyRemoteSubscribeFallback(index: number, showResult: boolean): boolean { + const engine = this.rtcEngine + if (engine == undefined) { + return false + } + const result: number = engine.setRemoteSubscribeFallbackOption(this.remoteFallbackOptions[index]) + Logger.info(TAG, `setRemoteSubscribeFallbackOption(${this.remoteFallbackOptions[index]}) result:${result}`) + if (showResult || result != Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(`setRemoteSubscribeFallbackOption: ${this.getResultText(result)}`) + } + 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) { + if (showResult) { + ShowToast.shortToast('Video image source is not ready') + } + return false + } + if (enabled && (!this.isJoin || !this.isLinking)) { + if (showResult) { + ShowToast.shortToast('Join as broadcaster first') + } + return false + } + const options: ImageTrackOptions = new ImageTrackOptions( + this.videoImageFilePath, VIDEO_IMAGE_FPS) + const result: number = engine.enableVideoImageSource(enabled, options) + Logger.info(TAG, `enableVideoImageSource(${enabled}) result:${result}`) + if (result == Constants.ErrorCode.ERR_OK) { + this.isVideoImageSourceEnabled = enabled + } + if (showResult || result != Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(`enableVideoImageSource: ${this.getResultText(result)}`) + } + return result == Constants.ErrorCode.ERR_OK + } aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear') @@ -43,12 +154,14 @@ 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) => { @@ -61,6 +174,8 @@ struct LiveStreaming { this.isJoin = false this.localUid = -1 this.remoteUid = -1 + this.isRemoteUserHighPriority = false + this.isVideoImageSourceEnabled = false } config.mEventHandler.onError = (err: number, message: string) => { Logger.info(TAG, "mEventHandler.onError: " + err + " message " + message); @@ -77,10 +192,14 @@ struct LiveStreaming { this.rtcEngine.setVideoEncoderConfiguration(encoderConfig) this.rtcEngine.enableVideo() this.rtcEngine.setDualStreamMode(Constants.SimulcastStreamMode.ENABLE) + this.prepareVideoImageFile(context) } aboutToDisappear(): void { if (this.rtcEngine != undefined) { + if (this.isVideoImageSourceEnabled) { + this.updateVideoImageSource(false, false) + } if (this.isJoin) { this.rtcEngine.stopPreview(); this.rtcEngine.leaveChannel(); @@ -108,49 +227,122 @@ struct LiveStreaming { Divider() - Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text($r('app.string.low_legacy_living')) - .flexGrow(1) - - Toggle({ type: ToggleType.Switch, isOn: this.lowLegacyLiving }) - .onChange((isOn) => { - this.lowLegacyLiving = isOn - if(!this.isLinking){ - let option = new ClientRoleOptions() - option.audienceLatencyLevel = this.lowLegacyLiving ? Constants.AudienceLatencyLevel.LOW_LATENCY : Constants.AudienceLatencyLevel.ULTRA_LOW_LATENCY - this.rtcEngine?.setClientRole(Constants.ClientRole.AUDIENCE, option) - } + Scroll() { + Column() { + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Text($r('app.string.low_legacy_living')) + .flexGrow(1) + + Toggle({ type: ToggleType.Switch, isOn: this.lowLegacyLiving }) + .onChange((isOn) => { + this.lowLegacyLiving = isOn + if (!this.isLinking) { + let option = new ClientRoleOptions() + option.audienceLatencyLevel = this.lowLegacyLiving ? + Constants.AudienceLatencyLevel.LOW_LATENCY : Constants.AudienceLatencyLevel.ULTRA_LOW_LATENCY + this.rtcEngine?.setClientRole(Constants.ClientRole.AUDIENCE, option) + } + }) + .flexGrow(0) + } + .padding({ + top: 8, + bottom: 8, + left: 12, + right: 12 }) - .flexGrow(0) - } - .padding({ - top: 8, - bottom: 8, - left: 12, - right: 12 - }) - Divider().margin({ left: 12, right: 12 }) + Divider().margin({ left: 12, right: 12 }) - Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text($r('app.string.low_stream')) - .flexGrow(1) + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Text($r('app.string.low_stream')) + .flexGrow(1) - Toggle({ type: ToggleType.Switch, isOn: this.pullLowStream }) - .onChange((isOn) => { - this.pullLowStream = isOn - this.rtcEngine?.setRemoteDefaultVideoStreamType(this.pullLowStream ? Constants.VideoStreamType.LOW : Constants.VideoStreamType.HIGH) + Toggle({ type: ToggleType.Switch, isOn: this.pullLowStream }) + .onChange((isOn) => { + this.pullLowStream = isOn + this.rtcEngine?.setRemoteDefaultVideoStreamType( + this.pullLowStream ? Constants.VideoStreamType.LOW : Constants.VideoStreamType.HIGH) + }) + .flexGrow(0) + } + .padding({ + top: 8, + bottom: 8, + left: 12, + right: 12 }) - .flexGrow(0) - } - .padding({ - top: 8, - bottom: 8, - left: 12, - right: 12 - }) - Divider().margin({ left: 12, right: 12 }) + Divider().margin({ left: 12, right: 12 }) + + Row() { + Text($r('app.string.remote_subscribe_fallback')) + Blank() + Select(this.remoteFallbackSelects.map((item: ResourceStr) => { + const option: SelectOption = { value: item } + return option + })) + .selected(this.remoteFallbackIndex) + .value(this.remoteFallbackSelects[this.remoteFallbackIndex]) + .enabled(!this.isJoin) + .backgroundColor($r('app.color.background_selector')) + .borderRadius(19) + .constraintSize({ minWidth: 150 }) + .onSelect((index: number) => { + if (this.applyRemoteSubscribeFallback(index, true)) { + this.remoteFallbackIndex = index + } + }) + } + .width('100%') + .height(54) + .padding({ left: 12, right: 12 }) + + 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) + + Toggle({ type: ToggleType.Switch, isOn: this.isVideoImageSourceEnabled }) + .enabled(this.isJoin && this.isLinking && this.isVideoImageSourceReady) + .onChange((isOn: boolean) => { + this.updateVideoImageSource(isOn, true) + }) + .flexGrow(0) + } + .padding({ + top: 8, + bottom: 8, + left: 12, + right: 12 + }) + } + .width('100%') + } + .layoutWeight(1) + .width('100%') + .scrollBar(BarState.Auto) }.width('100%').height('100%') } @@ -196,7 +388,12 @@ struct LiveStreaming { .id('button_to_linking') .enabled(this.isJoin) .onClick(() => { - this.isLinking = !this.isLinking + const nextIsLinking: boolean = !this.isLinking + if (!nextIsLinking && this.isVideoImageSourceEnabled && + !this.updateVideoImageSource(false, false)) { + return + } + this.isLinking = nextIsLinking this.rtcEngine?.updateChannelMediaOptions({ clientRoleType: this.isLinking ? Constants.ClientRole.BROADCASTER : Constants.ClientRole.AUDIENCE, publishCameraTrack: this.isLinking, @@ -266,6 +463,9 @@ struct LiveStreaming { mediaOption.clientRoleType = this.isLinking ? Constants.ClientRole.BROADCASTER : Constants.ClientRole.AUDIENCE; + if (!this.applyRemoteSubscribeFallback(this.remoteFallbackIndex, false)) { + return + } let token = await TokenUtils.genRtcToken(this.channelName, "") let ret = this.rtcEngine?.joinChannelWithOptions(token, this.channelName, 0, mediaOption) if (ret != Constants.ErrorCode.ERR_OK) { @@ -275,6 +475,9 @@ struct LiveStreaming { } this.rtcEngine?.startPreview() } else { + if (this.isVideoImageSourceEnabled) { + this.updateVideoImageSource(false, false) + } this.rtcEngine?.stopPreview() this.rtcEngine?.leaveChannel() } @@ -314,4 +517,4 @@ struct LiveStreaming { }), }) } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PlayAudioFiles.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PlayAudioFiles.ets index 58500e04b..248cd4273 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PlayAudioFiles.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PlayAudioFiles.ets @@ -6,18 +6,232 @@ import { AppID } from '../../common/KeyCenter'; import PermissionHelper from '../../util/PermissionHelper'; import { common } from '@kit.AbilityKit'; import { TokenUtils } from '../../util/TokenUtils'; +import { fileIo } from '@kit.CoreFileKit'; +import { BusinessError } from '@kit.BasicServicesKit'; const TAG: string = 'PlayAudioFiles' +const PRIMARY_AUDIO_EFFECT_ID: number = 0 +const SECONDARY_AUDIO_EFFECT_ID: number = 1 +const PRIMARY_AUDIO_EFFECT_FILE_NAME: string = 'effectA.wav' +const SECONDARY_AUDIO_EFFECT_FILE_NAME: string = 'music_1.m4a' @Entry @Component struct PlayAudioFiles { private rtcEngine: RtcEngine | undefined = undefined; + private primaryEffectFilePath: string = '' + private secondaryEffectFilePath: string = '' @State message: string = 'Hello World'; @State channelName: string = '' @State isJoin: boolean = false @State localUid: number = 0 @State remoteUidList: number[] = [] + @State isPrimaryEffectReady: boolean = false + @State isSecondaryEffectReady: boolean = false + @State isPrimaryEffectPreloaded: boolean = false + @State isSecondaryEffectPreloaded: boolean = false + @State effectVolume: number = 100 + + private getResultText(result: number): string { + return result == Constants.ErrorCode.ERR_OK ? 'success' : `error(${result})` + } + + private updateEffectFileState(soundId: number, filePath: string, isReady: boolean): void { + if (soundId == PRIMARY_AUDIO_EFFECT_ID) { + this.primaryEffectFilePath = filePath + this.isPrimaryEffectReady = isReady + } else { + this.secondaryEffectFilePath = filePath + this.isSecondaryEffectReady = isReady + } + } + + private prepareEffectFile(context: common.UIAbilityContext, soundId: number, fileName: string): void { + const targetPath: string = `${context.filesDir}/${fileName}` + context.resourceManager.getRawFileContent(fileName) + .then((buffer: Uint8Array) => { + const file = fileIo.openSync(targetPath, + fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.TRUNC) + let writtenLength: number = 0 + try { + writtenLength = fileIo.writeSync(file.fd, buffer.buffer as ArrayBuffer) + } finally { + fileIo.closeSync(file) + } + if (writtenLength != buffer.byteLength) { + Logger.error(TAG, `prepareEffectFile(${soundId}) incomplete:${writtenLength}/${buffer.byteLength}`) + ShowToast.longToast(`prepareEffectFile(${soundId}): incomplete write`) + return + } + this.updateEffectFileState(soundId, targetPath, true) + Logger.info(TAG, `prepareEffectFile(${soundId}) success:${targetPath}`) + }) + .catch((error: BusinessError) => { + this.updateEffectFileState(soundId, '', false) + Logger.error(TAG, `prepareEffectFile(${soundId}) failed. Code:${error.code}, message:${error.message}`) + ShowToast.longToast(`prepareEffectFile(${soundId}): error(${error.code})`) + }) + } + + private async startEffect(): Promise { + const engine = this.rtcEngine + const effectFilePath: string = this.primaryEffectFilePath + if (engine == undefined || !this.isPrimaryEffectReady || effectFilePath.length == 0) { + ShowToast.shortToast('Audio effect is not ready') + return + } + + let didPreload: boolean = false + if (!this.isPrimaryEffectPreloaded) { + const preloadResult: number = await engine.preloadEffect(PRIMARY_AUDIO_EFFECT_ID, effectFilePath, 0) + Logger.info(TAG, `preloadEffect result:${preloadResult}`) + if (preloadResult != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`preloadEffect: ${this.getResultText(preloadResult)}`) + return + } + this.isPrimaryEffectPreloaded = true + didPreload = true + } + + const playResult: number = await engine.playEffect( + PRIMARY_AUDIO_EFFECT_ID, + effectFilePath, + -1, + 1, + 0, + this.effectVolume, + true, + 0 + ) + Logger.info(TAG, `playEffect result:${playResult}`) + if (playResult == Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(didPreload ? 'preloadEffect/playEffect: success' : 'playEffect: success') + } else { + ShowToast.longToast(`playEffect: ${this.getResultText(playResult)}`) + } + } + + private async startAllEffects(): Promise { + const engine = this.rtcEngine + if (engine == undefined || !this.isPrimaryEffectReady || !this.isSecondaryEffectReady || + this.primaryEffectFilePath.length == 0 || this.secondaryEffectFilePath.length == 0) { + ShowToast.shortToast('Audio effects are not ready') + return + } + + if (!this.isPrimaryEffectPreloaded) { + const primaryResult: number = await engine.preloadEffect( + PRIMARY_AUDIO_EFFECT_ID, this.primaryEffectFilePath, 0) + Logger.info(TAG, `preloadEffect(${PRIMARY_AUDIO_EFFECT_ID}) result:${primaryResult}`) + if (primaryResult != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`preloadEffect(${PRIMARY_AUDIO_EFFECT_ID}): ${this.getResultText(primaryResult)}`) + return + } + this.isPrimaryEffectPreloaded = true + } + + if (!this.isSecondaryEffectPreloaded) { + const secondaryResult: number = await engine.preloadEffect( + SECONDARY_AUDIO_EFFECT_ID, this.secondaryEffectFilePath, 0) + Logger.info(TAG, `preloadEffect(${SECONDARY_AUDIO_EFFECT_ID}) result:${secondaryResult}`) + if (secondaryResult != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`preloadEffect(${SECONDARY_AUDIO_EFFECT_ID}): ${this.getResultText(secondaryResult)}`) + return + } + this.isSecondaryEffectPreloaded = true + } + + const result: number = engine.playAllEffects(-1, 1, 0, this.effectVolume, true) + Logger.info(TAG, `playAllEffects result:${result}`) + ShowToast.shortToast(`playAllEffects: ${this.getResultText(result)}`) + } + + private pauseEffect(): void { + if (this.rtcEngine == undefined) { + return + } + const result: number = this.rtcEngine.pauseEffect(PRIMARY_AUDIO_EFFECT_ID) + Logger.info(TAG, `pauseEffect result:${result}`) + ShowToast.shortToast(`pauseEffect: ${this.getResultText(result)}`) + } + + private resumeEffect(): void { + if (this.rtcEngine == undefined) { + return + } + const result: number = this.rtcEngine.resumeEffect(PRIMARY_AUDIO_EFFECT_ID) + Logger.info(TAG, `resumeEffect result:${result}`) + ShowToast.shortToast(`resumeEffect: ${this.getResultText(result)}`) + } + + private stopEffect(): void { + if (this.rtcEngine == undefined) { + return + } + const stopResult: number = this.rtcEngine.stopEffect(PRIMARY_AUDIO_EFFECT_ID) + const unloadResult: number = this.rtcEngine.unloadEffect(PRIMARY_AUDIO_EFFECT_ID) + this.isPrimaryEffectPreloaded = false + Logger.info(TAG, `stopEffect result:${stopResult}, unloadEffect result:${unloadResult}`) + ShowToast.shortToast( + `stopEffect: ${this.getResultText(stopResult)}, unloadEffect: ${this.getResultText(unloadResult)}`) + } + + private pauseAllEffects(): void { + if (this.rtcEngine == undefined) { + return + } + const result: number = this.rtcEngine.pauseAllEffects() + Logger.info(TAG, `pauseAllEffects result:${result}`) + ShowToast.shortToast(`pauseAllEffects: ${this.getResultText(result)}`) + } + + private resumeAllEffects(): void { + if (this.rtcEngine == undefined) { + return + } + const result: number = this.rtcEngine.resumeAllEffects() + Logger.info(TAG, `resumeAllEffects result:${result}`) + ShowToast.shortToast(`resumeAllEffects: ${this.getResultText(result)}`) + } + + private stopAllEffects(): void { + if (this.rtcEngine == undefined) { + return + } + const result: number = this.rtcEngine.stopAllEffects() + Logger.info(TAG, `stopAllEffects result:${result}`) + ShowToast.shortToast(`stopAllEffects: ${this.getResultText(result)}`) + } + + private unloadAllEffects(): void { + if (this.rtcEngine == undefined) { + return + } + const result: number = this.rtcEngine.unloadAllEffects() + Logger.info(TAG, `unloadAllEffects result:${result}`) + if (result == Constants.ErrorCode.ERR_OK) { + this.isPrimaryEffectPreloaded = false + this.isSecondaryEffectPreloaded = false + } + ShowToast.shortToast(`unloadAllEffects: ${this.getResultText(result)}`) + } + + private setEffectVolume(volume: number, showResult: boolean): void { + if (this.rtcEngine == undefined) { + return + } + this.effectVolume = Math.round(volume) + const setEffectsResult: number = this.rtcEngine.setEffectsVolume(this.effectVolume) + const setEffectResult: number = this.rtcEngine.setVolumeOfEffect(PRIMARY_AUDIO_EFFECT_ID, this.effectVolume) + const effectsVolume: number = this.rtcEngine.getEffectsVolume() + const effectVolume: number = this.rtcEngine.getVolumeOfEffect(PRIMARY_AUDIO_EFFECT_ID) + Logger.info(TAG, + `setEffectsVolume result:${setEffectsResult}, setVolumeOfEffect result:${setEffectResult}, ` + + `getEffectsVolume:${effectsVolume}, getVolumeOfEffect:${effectVolume}`) + if (showResult) { + ShowToast.shortToast(`effects volume: ${effectsVolume}, effect volume: ${effectVolume}`) + } + } aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear') @@ -53,15 +267,26 @@ struct PlayAudioFiles { Logger.info(TAG, "mEventHandler.onError: " + err + " message " + message); ShowToast.longToast("onError " + err + " : " + message) } + config.mEventHandler.onAudioMixingPositionChanged = (position: bigint) => { + Logger.info(TAG, "mEventHandler.onAudioMixingPositionChanged: " + position.toString()) + } config.mContext = context; Logger.info(TAG, "in thread create engine begin: "); this.rtcEngine = RtcEngine.create(config) this.rtcEngine.enableAudio() + this.prepareEffectFile(context, PRIMARY_AUDIO_EFFECT_ID, PRIMARY_AUDIO_EFFECT_FILE_NAME) + this.prepareEffectFile(context, SECONDARY_AUDIO_EFFECT_ID, SECONDARY_AUDIO_EFFECT_FILE_NAME) } aboutToDisappear(): void { if (this.rtcEngine != undefined) { + if (this.isPrimaryEffectPreloaded || this.isSecondaryEffectPreloaded) { + this.rtcEngine.stopAllEffects() + this.rtcEngine.unloadAllEffects() + this.isPrimaryEffectPreloaded = false + this.isSecondaryEffectPreloaded = false + } if (this.isJoin) { this.rtcEngine.leaveChannel(); } @@ -129,63 +354,62 @@ struct PlayAudioFiles { .align(Alignment.TopStart) .layoutWeight(1) - Column() { - Text($r('app.string.audio_mixing')).padding({ top: 6, bottom: 6 }) - Divider() - Row() { - Button($r('app.string.start')) - .layoutWeight(1) - .fontColor(Color.Blue) - .buttonStyle(ButtonStyleMode.NORMAL) - .onClick(() => { - this.rtcEngine?.startAudioMixing("https://webdemo.agora.io/audiomixing.mp3", false, -1, 0) - }) - Button($r('app.string.resume')) - .layoutWeight(1) - .fontColor(Color.Blue) - .buttonStyle(ButtonStyleMode.NORMAL) - .onClick(() => { - this.rtcEngine?.resumeAudioMixing() - }) - Button($r('app.string.pause')) - .layoutWeight(1) - .fontColor(Color.Blue) - .buttonStyle(ButtonStyleMode.NORMAL) - .onClick(() => { - this.rtcEngine?.pauseAudioMixing() - }) - Button($r('app.string.stop')) - .layoutWeight(1) - .fontColor(Color.Blue) - .buttonStyle(ButtonStyleMode.NORMAL) - .onClick(() => { - this.rtcEngine?.stopAudioMixing() - }) - }.alignItems(VerticalAlign.Center).padding({ top: 12, bottom: 6 }) + Scroll() { + Column() { + Text($r('app.string.audio_mixing')).padding({ top: 6, bottom: 6 }) + Divider() + Row() { + Button($r('app.string.start')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .onClick(() => { + this.rtcEngine?.startAudioMixing("https://webdemo.agora.io/audiomixing.mp3", false, -1, 0) + }) + Button($r('app.string.resume')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .onClick(() => { + this.rtcEngine?.resumeAudioMixing() + }) + Button($r('app.string.pause')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .onClick(() => { + this.rtcEngine?.pauseAudioMixing() + }) + Button($r('app.string.stop')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .onClick(() => { + this.rtcEngine?.stopAudioMixing() + }) + }.alignItems(VerticalAlign.Center).padding({ top: 12, bottom: 6 }) - Row() { - Text($r('app.string.volume')).layoutWeight(1) - Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { - this.rtcEngine?.adjustAudioMixingVolume(value) - }) - }.alignItems(VerticalAlign.Center) + Row() { + Text($r('app.string.volume')).layoutWeight(1) + Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { + this.rtcEngine?.adjustAudioMixingVolume(value) + }) + }.alignItems(VerticalAlign.Center) - Row() { - Text($r('app.string.play_volume')).layoutWeight(1) - Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { - this.rtcEngine?.adjustAudioMixingPlayoutVolume(value) - }) - }.alignItems(VerticalAlign.Center) + Row() { + Text($r('app.string.play_volume')).layoutWeight(1) + Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { + this.rtcEngine?.adjustAudioMixingPlayoutVolume(value) + }) + }.alignItems(VerticalAlign.Center) - Row() { - Text($r('app.string.publish_volume')).layoutWeight(1) - Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { - this.rtcEngine?.adjustAudioMixingPublishVolume(value) - }) - }.alignItems(VerticalAlign.Center) + Row() { + Text($r('app.string.publish_volume')).layoutWeight(1) + Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { + this.rtcEngine?.adjustAudioMixingPublishVolume(value) + }) + }.alignItems(VerticalAlign.Center) - //todo - if (false) { Text($r('app.string.audio_effect')).padding({ top: 12, bottom: 6 }) Divider() Row() { @@ -193,47 +417,104 @@ struct PlayAudioFiles { .layoutWeight(1) .fontColor(Color.Blue) .buttonStyle(ButtonStyleMode.NORMAL) - .onClick(() => { - this.rtcEngine?.playEffect(0, "https://webdemo.agora.io/ding.mp3", -1, 1, 0.0, 100, true, 0) + .enabled(this.isPrimaryEffectReady) + .onClick(async () => { + await this.startEffect() }) Button($r('app.string.resume')) .layoutWeight(1) .fontColor(Color.Blue) .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded) .onClick(() => { - + this.resumeEffect() }) Button($r('app.string.pause')) .layoutWeight(1) .fontColor(Color.Blue) .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded) .onClick(() => { - + this.pauseEffect() }) Button($r('app.string.stop')) .layoutWeight(1) .fontColor(Color.Blue) .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded) .onClick(() => { - + this.stopEffect() }) }.alignItems(VerticalAlign.Center).padding({ top: 12, bottom: 6 }) Row() { Text($r('app.string.volume')).layoutWeight(1) - Slider({ value: 100 }).layoutWeight(1.5).onChange((value) => { - - }) + Slider({ value: this.effectVolume, min: 0, max: 100, step: 1 }) + .layoutWeight(1.5) + .enabled(this.isPrimaryEffectPreloaded) + .onChange((value: number, mode: SliderChangeMode) => { + this.setEffectVolume(value, mode == SliderChangeMode.End || mode == SliderChangeMode.Click) + }) }.alignItems(VerticalAlign.Center) + + Text($r('app.string.all_audio_effects')).padding({ top: 12, bottom: 6 }) + Divider() + Row() { + Button($r('app.string.play_all')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectReady && this.isSecondaryEffectReady) + .onClick(async () => { + await this.startAllEffects() + }) + Button($r('app.string.resume_all')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded || this.isSecondaryEffectPreloaded) + .onClick(() => { + this.resumeAllEffects() + }) + Button($r('app.string.pause_all')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded || this.isSecondaryEffectPreloaded) + .onClick(() => { + this.pauseAllEffects() + }) + }.alignItems(VerticalAlign.Center).padding({ top: 12, bottom: 6 }) + Row() { + Button($r('app.string.stop_all')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded || this.isSecondaryEffectPreloaded) + .onClick(() => { + this.stopAllEffects() + }) + Button($r('app.string.unload_all')) + .layoutWeight(1) + .fontColor(Color.Blue) + .buttonStyle(ButtonStyleMode.NORMAL) + .enabled(this.isPrimaryEffectPreloaded || this.isSecondaryEffectPreloaded) + .onClick(() => { + this.unloadAllEffects() + }) + }.alignItems(VerticalAlign.Center).padding({ bottom: 6 }) } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding({ + left: 12, + right: 12, + top: 6, + bottom: 6 + }) } - .alignItems(HorizontalAlign.Start) .layoutWeight(3) - .padding({ - left: 12, - right: 12, - top: 6, - bottom: 6 - }) + .width('100%') + .scrollBar(BarState.Auto) Row() { TextInput({ placeholder: $r('app.string.text_input_channel_name') }) @@ -292,4 +573,4 @@ struct PlayAudioFiles { } .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PreCallTest.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PreCallTest.ets index 26fc3c965..583262d39 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PreCallTest.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/PreCallTest.ets @@ -1,4 +1,5 @@ import { + AudioVolumeInfo, Constants, LastmileProbeConfig, LastmileProbeResult, @@ -25,6 +26,9 @@ struct PlayAudioFiles { @State isAudioEchoTesting: boolean = false @State isVideoEchoTesting: boolean = false @State audioEchoTestingText: string = '' + @State isRecordingDeviceTesting: boolean = false + @State isRecordingSignalMuted: boolean = false + @State recordingVolume: number = 0 aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear') @@ -36,6 +40,13 @@ struct PlayAudioFiles { Logger.info(TAG, "mEventHandler.onError: " + err + " message " + message); ShowToast.longToast("onError " + err + " : " + message) } + config.mEventHandler.onAudioVolumeIndication = (speakers: Array, totalVolume: number) => { + Logger.info(TAG, + `mEventHandler.onAudioVolumeIndication totalVolume:${totalVolume} speakers:${JSON.stringify(speakers)}`) + if (this.isRecordingDeviceTesting) { + this.recordingVolume = totalVolume + } + } config.mEventHandler.onLastmileProbeResult = (result: LastmileProbeResult) => { let map = new Map() map.set("Rtt", result.rtt + 'ms') @@ -92,6 +103,12 @@ struct PlayAudioFiles { this.audioEchoCountDown = undefined } if (this.rtcEngine != undefined) { + if (this.isRecordingDeviceTesting) { + if (this.isRecordingSignalMuted) { + this.rtcEngine.muteRecordingSignal(false) + } + this.rtcEngine.stopRecordingDeviceTest() + } Logger.info(TAG, "destroy begin") RtcEngine.destroy().then(() => { Logger.info(TAG, "destroy done") @@ -112,6 +129,54 @@ struct PlayAudioFiles { this.lastmileResultText = text } + startRecordingDeviceTest(): void { + if (this.rtcEngine == undefined) { + return + } + let result = this.rtcEngine.startRecordingDeviceTest(200) + Logger.info(TAG, `startRecordingDeviceTest result:${result}`) + if (result == 0) { + this.isRecordingDeviceTesting = true + this.recordingVolume = 0 + ShowToast.shortToast('Recording device test started') + } else { + ShowToast.longToast(`startRecordingDeviceTest failed: ${result}`) + } + } + + stopRecordingDeviceTest(): void { + if (this.rtcEngine == undefined) { + return + } + if (this.isRecordingSignalMuted) { + this.rtcEngine.muteRecordingSignal(false) + } + let result = this.rtcEngine.stopRecordingDeviceTest() + Logger.info(TAG, `stopRecordingDeviceTest result:${result}`) + if (result == 0) { + this.isRecordingDeviceTesting = false + this.isRecordingSignalMuted = false + this.recordingVolume = 0 + ShowToast.shortToast('Recording device test stopped') + } else { + ShowToast.longToast(`stopRecordingDeviceTest failed: ${result}`) + } + } + + setRecordingSignalMuted(muted: boolean): void { + if (this.rtcEngine == undefined) { + return + } + let result = this.rtcEngine.muteRecordingSignal(muted) + Logger.info(TAG, `muteRecordingSignal muted:${muted} result:${result}`) + if (result == 0) { + this.isRecordingSignalMuted = muted + ShowToast.shortToast(muted ? 'Recording signal muted' : 'Recording signal resumed') + } else { + ShowToast.longToast(`muteRecordingSignal failed: ${result}`) + } + } + build() { Column() { TitleBar({ @@ -151,6 +216,39 @@ struct PlayAudioFiles { bottom: 6 }) + Column() { + Text('Recording Device Test').padding({ top: 6, bottom: 6 }) + Divider() + Button(this.isRecordingDeviceTesting ? $r('app.string.stop') : $r('app.string.start')) + .margin({ top: 6, bottom: 6 }) + .onClick(() => { + if (this.isRecordingDeviceTesting) { + this.stopRecordingDeviceTest() + } else { + this.startRecordingDeviceTest() + } + }) + Text(`Microphone Volume: ${this.recordingVolume}`).padding({ top: 6, bottom: 6 }) + Row() { + Text('Mute Recording Signal').layoutWeight(1) + Toggle({ type: ToggleType.Switch, isOn: this.isRecordingSignalMuted }) + .enabled(this.isRecordingDeviceTesting) + .onChange((isOn: boolean) => { + this.setRecordingSignalMuted(isOn) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + .padding({ + left: 12, + right: 12, + top: 6, + bottom: 6 + }) + //todo if (false) { Column() { @@ -227,4 +325,4 @@ struct PlayAudioFiles { .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/RTMPStreaming.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/RTMPStreaming.ets index f5dd82993..af3bf0093 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/RTMPStreaming.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/RTMPStreaming.ets @@ -40,19 +40,19 @@ struct RTMPStreaming { config.mEventHandler = {}; config.mEventHandler.onUserJoined = (uid: number, collapse: number) => { Logger.info(TAG, "mEventHandler.onUserJoined: " + uid + " , " + collapse); - // if (this.remoteUid == -1) { - // this.remoteUid = uid - // let user = new TranscodingUser(); - // user.uid = uid; - // user.x = 0; - // user.y = this.transcodingParameters.height / 2; - // user.width = this.transcodingParameters.width; - // user.height = this.transcodingParameters.height / 2; - // user.zOrder = 1; - // user.alpha = 1.0 - // user.audioChannel = 2; - // this.transcodingParameters.transcodingUsers.set(uid,user) - // } + if (this.remoteUid == -1) { + this.remoteUid = uid + let user = new TranscodingUser(); + user.uid = uid; + user.x = 0; + user.y = this.transcodingParameters.height / 2; + user.width = this.transcodingParameters.width; + user.height = this.transcodingParameters.height / 2; + user.zOrder = 1; + user.alpha = 1.0 + user.audioChannel = 2; + this.transcodingParameters.transcodingUsers.set(uid,user) + } }; config.mEventHandler.onUserOffline = (uid: number, reason: number) => { Logger.info(TAG, "mEventHandler.onUserOffline: " + uid + " , " + reason) diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/UserAccount.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/UserAccount.ets new file mode 100644 index 000000000..c530b1c89 --- /dev/null +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/UserAccount.ets @@ -0,0 +1,351 @@ +import { + ChannelMediaOptions, + Constants, + IRtcEngineEventHandler, + RtcConnection, + RtcEngine, + RtcEngineConfig, + RtcEngineEx, + UserInfo, + VideoCanvas, + VideoEncoderConfiguration +} from '@shengwang/rtc-full'; +import { common } from '@kit.AbilityKit'; +import { TitleBar } from '../../common/TitleBar'; +import { AppID } from '../../common/KeyCenter'; +import { GlobalInfo } from '../Data'; +import Logger from '../../util/Logger'; +import PermissionHelper from '../../util/PermissionHelper'; +import ShowToast from '../../util/ShowToast'; +import { TokenUtils } from '../../util/TokenUtils'; + +const TAG: string = 'UserAccount' +const VIDEO_PREVIEW_ASPECT_RATIO: number = 1.2676 + +@Entry +@Component +struct UserAccount { + private rtcEngine: RtcEngineEx | undefined = undefined + private rtcConnection: RtcConnection = new RtcConnection('', 0) + private localXComponentController: XComponentController = new XComponentController() + private remoteXComponentController: XComponentController = new XComponentController() + + @State channelName: string = '' + @State userAccount: string = '' + @State isJoined: boolean = false + @State localUid: number = 0 + @State remoteUid: number = 0 + @State mappedUid: number = 0 + + private showResult(apiName: string, result: number): boolean { + Logger.info(TAG, `${apiName} result:${result}`) + if (result == Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(`${apiName}: success`) + return true + } + ShowToast.longToast(`${apiName} failed: ${result} : ${RtcEngine.getErrorDescription(result)}`) + return false + } + + private validateInput(): boolean { + if (this.channelName.length == 0) { + ShowToast.shortToast($r('app.string.channel_name_empty')) + return false + } + if (this.userAccount.length == 0) { + ShowToast.shortToast($r('app.string.user_account_empty')) + return false + } + return true + } + + private queryUserInfo(uid: number, userAccount: string): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + const infoByAccount = new UserInfo(0, '') + const accountResult = engine.getUserInfoByUserAccountEx(userAccount, infoByAccount, this.rtcConnection) + const infoByUid = new UserInfo(0, '') + const uidResult = engine.getUserInfoByUidEx(uid, infoByUid, this.rtcConnection) + Logger.info(TAG, `getUserInfoByUserAccountEx result:${accountResult}, uid:${infoByAccount.uid}`) + Logger.info(TAG, `getUserInfoByUidEx result:${uidResult}, userAccount:${infoByUid.userAccount}`) + if (accountResult == Constants.ErrorCode.ERR_OK && uidResult == Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`User Account: ${infoByUid.userAccount} -> ${infoByAccount.uid}`) + } else { + ShowToast.longToast(`Get UserInfo failed: account=${accountResult}, uid=${uidResult}`) + } + } + + private createExEventHandler(): IRtcEngineEventHandler { + const eventHandler: IRtcEngineEventHandler = {} + eventHandler.onJoinChannelSuccess = (channel: string, uid: number, elapsed: number) => { + Logger.info(TAG, `onJoinChannelSuccess channel:${channel}, uid:${uid}, elapsed:${elapsed}`) + this.rtcConnection.channelId = channel + this.rtcConnection.localUid = uid + this.localUid = uid + this.mappedUid = uid + this.isJoined = true + } + eventHandler.onLocalUserRegistered = (uid: number, userAccount: string) => { + Logger.info(TAG, `onLocalUserRegistered userAccount:${userAccount}, uid:${uid}`) + this.mappedUid = uid + ShowToast.longToast(`onLocalUserRegistered: ${userAccount} -> ${uid}`) + } + eventHandler.onUserJoined = (uid: number, elapsed: number) => { + Logger.info(TAG, `onUserJoined uid:${uid}, elapsed:${elapsed}`) + if (this.remoteUid == 0) { + this.remoteUid = uid + } + } + eventHandler.onUserInfoUpdated = (uid: number, userInfo: UserInfo) => { + Logger.info(TAG, `onUserInfoUpdated userAccount:${userInfo.userAccount}, uid:${uid}`) + this.queryUserInfo(uid, userInfo.userAccount) + } + eventHandler.onUserOffline = (uid: number, reason: number) => { + Logger.info(TAG, `onUserOffline uid:${uid}, reason:${reason}`) + if (this.remoteUid == uid) { + this.remoteUid = 0 + } + } + eventHandler.onLeaveChannel = () => { + Logger.info(TAG, 'onLeaveChannel') + this.isJoined = false + this.localUid = 0 + this.remoteUid = 0 + this.mappedUid = 0 + this.rtcConnection.localUid = 0 + } + eventHandler.onError = (err: number, message: string) => { + Logger.error(TAG, `onError err:${err}, message:${message}`) + ShowToast.longToast(`onError ${err}: ${message}`) + } + return eventHandler + } + + private registerUserAccount(): void { + if (this.userAccount.length == 0) { + ShowToast.shortToast($r('app.string.user_account_empty')) + return + } + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.showResult('registerLocalUserAccount', engine.registerLocalUserAccount(AppID, this.userAccount)) + } + + private async preloadChannel(): Promise { + if (!this.validateInput()) { + return + } + const engine = this.rtcEngine + if (engine == undefined) { + return + } + const token = await TokenUtils.genRtcToken(this.channelName, this.userAccount) + this.showResult('preloadChannelWithUserAccount', + engine.preloadChannelWithUserAccount(token, this.channelName, this.userAccount)) + } + + private async joinChannelEx(): Promise { + if (!this.validateInput()) { + return + } + if (!(await PermissionHelper.checkPermissions( + getContext(this) as common.UIAbilityContext, + ['ohos.permission.MICROPHONE', 'ohos.permission.CAMERA'] + ))) { + ShowToast.shortToast('Permission leak!') + return + } + const engine = this.rtcEngine + if (engine == undefined) { + return + } + const options = new ChannelMediaOptions() + options.publishCameraTrack = true + options.publishMicrophoneTrack = true + options.autoSubscribeVideo = true + options.autoSubscribeAudio = true + options.channelProfile = Constants.ChannelProfile.LIVE_BROADCASTING + options.clientRoleType = Constants.ClientRole.BROADCASTER + this.rtcConnection.channelId = this.channelName + this.rtcConnection.localUid = 0 + const token = await TokenUtils.genRtcToken(this.channelName, this.userAccount) + const result = engine.joinChannelWithUserAccountEx( + token, this.channelName, this.userAccount, options, this.createExEventHandler()) + if (this.showResult('joinChannelWithUserAccountEx', result)) { + engine.startPreview() + } + } + + private leaveChannelEx(): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + Logger.info(TAG, + `leaveChannelEx channel:${this.rtcConnection.channelId}, localUid:${this.rtcConnection.localUid}`) + const result = engine.leaveChannelEx(this.rtcConnection) + if (this.showResult('leaveChannelEx', result)) { + engine.stopPreview() + } + } + + aboutToAppear(): void { + const config = new RtcEngineConfig() + config.mAppId = AppID + config.mContext = getContext(this) as common.UIAbilityContext + config.mEventHandler = {} + config.mEventHandler.onLocalUserRegistered = (uid: number, userAccount: string) => { + Logger.info(TAG, `onLocalUserRegistered userAccount:${userAccount}, uid:${uid}`) + this.mappedUid = uid + ShowToast.longToast(`onLocalUserRegistered: ${userAccount} -> ${uid}`) + } + config.mEventHandler.onError = (err: number, message: string) => { + Logger.error(TAG, `onError err:${err}, message:${message}`) + ShowToast.longToast(`onError ${err}: ${message}`) + } + this.rtcEngine = RtcEngine.create(config) as RtcEngineEx + const 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() + } + + aboutToDisappear(): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + if (this.isJoined) { + engine.stopPreview() + engine.leaveChannelEx(this.rtcConnection) + } + RtcEngine.destroy().then(() => { + Logger.info(TAG, 'destroy done') + }) + this.rtcEngine = undefined + } + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + TitleBar({ + title: $r('app.string.item_useraccount'), + showBack: true + }).flexGrow(0) + + Column() { + Row() { + if (this.localUid != 0) { + XComponent({ + id: 'user_account_local', + type: XComponentType.SURFACE, + controller: this.localXComponentController + }) + .onLoad(() => { + const surfaceId = this.localXComponentController.getXComponentSurfaceId() + const canvas = new VideoCanvas(surfaceId, true) + canvas.uid = this.localUid + canvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN + canvas.mirrorMode = 0 + this.rtcEngine?.setupLocalVideo(canvas) + }) + .width('50%') + } + + if (this.remoteUid != 0) { + XComponent({ + id: 'user_account_remote', + type: XComponentType.SURFACE, + controller: this.remoteXComponentController + }) + .onLoad(() => { + const surfaceId = this.remoteXComponentController.getXComponentSurfaceId() + const canvas = new VideoCanvas(surfaceId, true) + canvas.uid = this.remoteUid + canvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN + canvas.mirrorMode = 0 + this.rtcEngine?.setupRemoteVideoEx(canvas, this.rtcConnection) + }) + .width('50%') + } + } + .align(Alignment.TopStart) + .width('100%') + .aspectRatio(VIDEO_PREVIEW_ASPECT_RATIO) + } + .align(Alignment.TopStart) + .flexGrow(1) + + Column({ space: 8 }) { + TextInput({ placeholder: $r('app.string.text_input_channel_name') }) + .id('input_user_account_channel') + .enabled(!this.isJoined) + .enableKeyboardOnFocus(false) + .maxLength(64) + .onChange((value: string) => { + this.channelName = value + }) + + TextInput({ placeholder: $r('app.string.text_input_user_account') }) + .id('input_user_account') + .enabled(!this.isJoined) + .enableKeyboardOnFocus(false) + .maxLength(255) + .onChange((value: string) => { + this.userAccount = value + this.mappedUid = 0 + }) + + if (this.mappedUid != 0) { + Row() { + Text($r('app.string.mapped_int_uid')) + Text(`: ${this.mappedUid}`) + } + .width('100%') + } + + Row({ space: 8 }) { + Button($r('app.string.register_user_account')) + .id('button_register_user_account') + .enabled(!this.isJoined) + .onClick(() => { + this.registerUserAccount() + }) + .layoutWeight(1) + + Button($r('app.string.preload_user_account_channel')) + .id('button_preload_user_account_channel') + .enabled(!this.isJoined) + .onClick(async () => { + await this.preloadChannel() + }) + .layoutWeight(1) + } + .width('100%') + + Button(this.isJoined ? $r('app.string.leave') : $r('app.string.join')) + .id('button_user_account_join') + .onClick(async () => { + focusControl.requestFocus('button_user_account_join') + if (this.isJoined) { + this.leaveChannelEx() + } else { + await this.joinChannelEx() + } + }) + .width('100%') + } + .flexGrow(0) + .width('100%') + .padding({ left: 12, right: 12, top: 10, bottom: 12 }) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_shallow_grey')) + } +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/VideoBeauty.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/VideoBeauty.ets new file mode 100644 index 000000000..2f1d6744c --- /dev/null +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/VideoBeauty.ets @@ -0,0 +1,386 @@ +import { + BeautyOptions, + ChannelMediaOptions, + Constants, + RtcEngine, + RtcEngineConfig, + VideoCanvas, + VideoEncoderConfiguration +} from '@shengwang/rtc-full'; +import { common } from '@kit.AbilityKit'; +import { TitleBar } from '../../common/TitleBar'; +import { AppID } from '../../common/KeyCenter'; +import { GlobalInfo } from '../Data'; +import Logger from '../../util/Logger'; +import PermissionHelper from '../../util/PermissionHelper'; +import ShowToast from '../../util/ShowToast'; +import { TokenUtils } from '../../util/TokenUtils'; + +const TAG: string = 'VideoBeauty' +const VIDEO_PREVIEW_ASPECT_RATIO: number = 1.2676 + +@Entry +@Component +struct VideoBeauty { + private rtcEngine: RtcEngine | undefined = undefined + private localXComponentController: XComponentController = new XComponentController() + private remoteXComponentController: XComponentController = new XComponentController() + private contrastLevels: number[] = [ + BeautyOptions.LIGHTENING_CONTRAST_LOW, + BeautyOptions.LIGHTENING_CONTRAST_NORMAL, + BeautyOptions.LIGHTENING_CONTRAST_HIGH + ] + private contrastNames: ResourceStr[] = [ + $r('app.string.beauty_contrast_low'), + $r('app.string.beauty_contrast_normal'), + $r('app.string.beauty_contrast_high') + ] + + @State channelName: string = '' + @State isJoin: boolean = false + @State localUid: number = 0 + @State remoteUid: number = 0 + @State beautyEnabled: boolean = false + @State contrastIndex: number = 1 + @State lighteningLevel: number = 0.6 + @State smoothnessLevel: number = 0.5 + @State rednessLevel: number = 0.1 + @State sharpnessLevel: number = 0.3 + + private applyBeautyEffect(detail: string, showToast: boolean): boolean { + const engine = this.rtcEngine + if (engine == undefined) { + return false + } + const options = new BeautyOptions( + this.contrastLevels[this.contrastIndex], + this.lighteningLevel, + this.smoothnessLevel, + this.rednessLevel, + this.sharpnessLevel + ) + const result = engine.setBeautyEffectOptions( + this.beautyEnabled, + options, + Constants.MediaSourceType.PRIMARY_CAMERA + ) + Logger.info(TAG, `setBeautyEffectOptions ${detail} result:${result}`) + if (showToast) { + if (result == Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(`setBeautyEffectOptions: ${detail}`) + } else { + ShowToast.longToast(`setBeautyEffectOptions failed: ${result} : ${RtcEngine.getErrorDescription(result)}`) + } + } + return result == Constants.ErrorCode.ERR_OK + } + + private resetBeautyEffect(): void { + if (this.beautyEnabled) { + this.beautyEnabled = false + this.applyBeautyEffect('enabled=false', false) + } + } + + private leaveChannel(): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.resetBeautyEffect() + engine.stopPreview() + const result = engine.leaveChannel() + if (result != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`leaveChannel failed: ${result} : ${RtcEngine.getErrorDescription(result)}`) + } + } + + aboutToAppear(): void { + const config = new RtcEngineConfig() + config.mAppId = AppID + config.mContext = getContext(this) as common.UIAbilityContext + config.mEventHandler = {} + config.mEventHandler.onUserJoined = (uid: number, elapsed: number) => { + Logger.info(TAG, `onUserJoined uid:${uid}, elapsed:${elapsed}`) + if (this.remoteUid == 0) { + this.remoteUid = uid + } + } + config.mEventHandler.onUserOffline = (uid: number, reason: number) => { + Logger.info(TAG, `onUserOffline uid:${uid}, reason:${reason}`) + if (this.remoteUid == uid) { + this.remoteUid = 0 + } + } + config.mEventHandler.onJoinChannelSuccess = (channel: string, uid: number, elapsed: number) => { + Logger.info(TAG, `onJoinChannelSuccess channel:${channel}, uid:${uid}, elapsed:${elapsed}`) + this.isJoin = true + this.localUid = uid + } + config.mEventHandler.onLeaveChannel = () => { + Logger.info(TAG, 'onLeaveChannel') + this.isJoin = false + this.localUid = 0 + this.remoteUid = 0 + this.beautyEnabled = false + } + config.mEventHandler.onError = (err: number, message: string) => { + Logger.error(TAG, `onError err:${err}, message:${message}`) + ShowToast.longToast(`onError ${err}: ${message}`) + } + + this.rtcEngine = RtcEngine.create(config) + const 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() + } + + aboutToDisappear(): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.resetBeautyEffect() + if (this.isJoin) { + engine.stopPreview() + engine.leaveChannel() + } + RtcEngine.destroy().then(() => { + Logger.info(TAG, 'destroy done') + }) + this.rtcEngine = undefined + } + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + TitleBar({ + title: $r('app.string.item_videobeauty'), + showBack: true + }).flexGrow(0) + + Column() { + Row() { + if (this.localUid != 0) { + XComponent({ + id: 'video_beauty_local', + type: XComponentType.SURFACE, + controller: this.localXComponentController + }) + .onLoad(() => { + const surfaceId = this.localXComponentController.getXComponentSurfaceId() + const canvas = new VideoCanvas(surfaceId, true) + canvas.uid = this.localUid + canvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN + canvas.mirrorMode = 0 + this.rtcEngine?.setupLocalVideo(canvas) + }) + .width('50%') + } + + if (this.remoteUid != 0) { + XComponent({ + id: 'video_beauty_remote', + type: XComponentType.SURFACE, + controller: this.remoteXComponentController + }) + .onLoad(() => { + const surfaceId = this.remoteXComponentController.getXComponentSurfaceId() + const canvas = new VideoCanvas(surfaceId, true) + canvas.uid = this.remoteUid + canvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN + canvas.mirrorMode = 0 + this.rtcEngine?.setupRemoteVideo(canvas) + }) + .width('50%') + } + } + .align(Alignment.TopStart) + .width('100%') + .aspectRatio(VIDEO_PREVIEW_ASPECT_RATIO) + } + .align(Alignment.TopStart) + .flexGrow(0) + + Scroll() { + Column({ space: 8 }) { + Row() { + Text($r('app.string.beauty_effect')).layoutWeight(1) + Toggle({ type: ToggleType.Switch, isOn: this.beautyEnabled }) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .onChange((isOn: boolean) => { + const previousValue = this.beautyEnabled + this.beautyEnabled = isOn + if (!this.applyBeautyEffect(`enabled=${isOn}`, true)) { + this.beautyEnabled = previousValue + } + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.beauty_contrast')).layoutWeight(1) + Select(this.contrastNames.map((name: ResourceStr) => { + const option: SelectOption = { value: name } + return option + })) + .selected(this.contrastIndex) + .value(this.contrastNames[this.contrastIndex]) + .enabled(this.isJoin && this.beautyEnabled) + .opacity(this.isJoin && this.beautyEnabled ? 1 : 0.4) + .width('54%') + .onSelect((index: number, text: string) => { + const previousIndex = this.contrastIndex + this.contrastIndex = index + if (!this.applyBeautyEffect(`lighteningContrastLevel=${text}`, true)) { + this.contrastIndex = previousIndex + } + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.beauty_lightening')).layoutWeight(1) + Text(this.lighteningLevel.toFixed(1)).width(32) + Slider({ value: this.lighteningLevel, min: 0, max: 1, step: 0.1 }) + .enabled(this.isJoin && this.beautyEnabled) + .opacity(this.isJoin && this.beautyEnabled ? 1 : 0.4) + .width('54%') + .onChange((value: number, mode: SliderChangeMode) => { + this.lighteningLevel = Math.round(value * 10) / 10 + this.applyBeautyEffect( + `lighteningLevel=${this.lighteningLevel.toFixed(1)}`, + mode == SliderChangeMode.End || mode == SliderChangeMode.Click + ) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.beauty_smoothness')).layoutWeight(1) + Text(this.smoothnessLevel.toFixed(1)).width(32) + Slider({ value: this.smoothnessLevel, min: 0, max: 1, step: 0.1 }) + .enabled(this.isJoin && this.beautyEnabled) + .opacity(this.isJoin && this.beautyEnabled ? 1 : 0.4) + .width('54%') + .onChange((value: number, mode: SliderChangeMode) => { + this.smoothnessLevel = Math.round(value * 10) / 10 + this.applyBeautyEffect( + `smoothnessLevel=${this.smoothnessLevel.toFixed(1)}`, + mode == SliderChangeMode.End || mode == SliderChangeMode.Click + ) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.beauty_redness')).layoutWeight(1) + Text(this.rednessLevel.toFixed(1)).width(32) + Slider({ value: this.rednessLevel, min: 0, max: 1, step: 0.1 }) + .enabled(this.isJoin && this.beautyEnabled) + .opacity(this.isJoin && this.beautyEnabled ? 1 : 0.4) + .width('54%') + .onChange((value: number, mode: SliderChangeMode) => { + this.rednessLevel = Math.round(value * 10) / 10 + this.applyBeautyEffect( + `rednessLevel=${this.rednessLevel.toFixed(1)}`, + mode == SliderChangeMode.End || mode == SliderChangeMode.Click + ) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.beauty_sharpness')).layoutWeight(1) + Text(this.sharpnessLevel.toFixed(1)).width(32) + Slider({ value: this.sharpnessLevel, min: 0, max: 1, step: 0.1 }) + .enabled(this.isJoin && this.beautyEnabled) + .opacity(this.isJoin && this.beautyEnabled ? 1 : 0.4) + .width('54%') + .onChange((value: number, mode: SliderChangeMode) => { + this.sharpnessLevel = Math.round(value * 10) / 10 + this.applyBeautyEffect( + `sharpnessLevel=${this.sharpnessLevel.toFixed(1)}`, + mode == SliderChangeMode.End || mode == SliderChangeMode.Click + ) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + } + .width('100%') + .padding({ left: 12, right: 12, top: 8, bottom: 8 }) + } + .layoutWeight(1) + .width('100%') + .scrollBar(BarState.Auto) + + Row() { + TextInput({ placeholder: $r('app.string.text_input_channel_name') }) + .id('input_video_beauty_channel') + .enabled(!this.isJoin) + .enableKeyboardOnFocus(false) + .onChange((value: string) => { + this.channelName = value + }) + .layoutWeight(1) + .margin({ right: 6 }) + + Button(this.isJoin ? $r('app.string.leave') : $r('app.string.join')) + .id('button_video_beauty_join') + .onClick(async () => { + focusControl.requestFocus('button_video_beauty_join') + const engine = this.rtcEngine + if (engine == undefined) { + return + } + if (this.isJoin) { + this.leaveChannel() + return + } + if (this.channelName.length == 0) { + ShowToast.shortToast($r('app.string.channel_name_empty')) + return + } + if (!(await PermissionHelper.checkPermissions( + getContext(this) as common.UIAbilityContext, + ['ohos.permission.MICROPHONE', 'ohos.permission.CAMERA'] + ))) { + ShowToast.shortToast('Permission leak!') + return + } + + const options = new ChannelMediaOptions() + options.publishCameraTrack = true + options.publishMicrophoneTrack = true + options.autoSubscribeVideo = true + options.autoSubscribeAudio = true + options.channelProfile = Constants.ChannelProfile.LIVE_BROADCASTING + options.clientRoleType = Constants.ClientRole.BROADCASTER + + const token = await TokenUtils.genRtcToken(this.channelName, '') + const result = engine.joinChannelWithOptions(token, this.channelName, 0, options) + if (result != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`joinChannelWithOptions failed: ${result} : ${RtcEngine.getErrorDescription(result)}`) + return + } + engine.startPreview() + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_shallow_grey')) + } +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/VoiceEffects.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/VoiceEffects.ets new file mode 100644 index 000000000..01d2674bf --- /dev/null +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/advance/VoiceEffects.ets @@ -0,0 +1,509 @@ +import { + ChannelMediaOptions, + Constants, + RtcEngine, + RtcEngineConfig +} from '@shengwang/rtc-full'; +import { common } from '@kit.AbilityKit'; +import { TitleBar } from '../../common/TitleBar'; +import { AppID } from '../../common/KeyCenter'; +import Logger from '../../util/Logger'; +import PermissionHelper from '../../util/PermissionHelper'; +import ShowToast from '../../util/ShowToast'; +import { TokenUtils } from '../../util/TokenUtils'; + +const TAG: string = 'VoiceEffects' + +@Entry +@Component +struct VoiceEffects { + private rtcEngine: RtcEngine | undefined = undefined + private voiceBeautifierPresets: Constants.VoiceBeautifierPreset[] = [ + Constants.VoiceBeautifierPreset.OFF, + Constants.VoiceBeautifierPreset.CHAT_BEAUTIFIER_MAGNETIC, + Constants.VoiceBeautifierPreset.CHAT_BEAUTIFIER_FRESH, + Constants.VoiceBeautifierPreset.CHAT_BEAUTIFIER_VITALITY, + Constants.VoiceBeautifierPreset.SINGING_BEAUTIFIER, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_VIGOROUS, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_DEEP, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_MELLOW, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_FALSETTO, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_FULL, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_CLEAR, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_RESOUNDING, + Constants.VoiceBeautifierPreset.TIMBRE_TRANSFORMATION_RINGING + ] + private voiceBeautifierNames: ResourceStr[] = [ + 'OFF', + 'MAGNETIC', + 'FRESH', + 'VITALITY', + 'SINGING', + 'VIGOROUS', + 'DEEP', + 'MELLOW', + 'FALSETTO', + 'FULL', + 'CLEAR', + 'RESOUNDING', + 'RINGING' + ] + private audioEffectPresets: Constants.AudioEffectPreset[] = [ + Constants.AudioEffectPreset.AUDIO_EFFECT_OFF, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_KTV, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_VOCAL_CONCERT, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_STUDIO, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_PHONOGRAPH, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_VIRTUAL_STEREO, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_SPACIAL, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_ETHEREAL, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_3D_VOICE, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_VIRTUAL_SURROUND_SOUND, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_CHORUS, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_BATHROOM, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_STAIRWELL, + Constants.AudioEffectPreset.ROOM_ACOUSTICS_NATURE, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_UNCLE, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_OLDMAN, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_BOY, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_SISTER, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_GIRL, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_PIGKING, + Constants.AudioEffectPreset.VOICE_CHANGER_EFFECT_HULK, + Constants.AudioEffectPreset.STYLE_TRANSFORMATION_RNB, + Constants.AudioEffectPreset.STYLE_TRANSFORMATION_POPULAR, + Constants.AudioEffectPreset.PITCH_CORRECTION + ] + private audioEffectNames: ResourceStr[] = [ + 'OFF', + 'KTV', + 'VOCAL_CONCERT', + 'STUDIO', + 'PHONOGRAPH', + 'VIRTUAL_STEREO', + 'SPACIAL', + 'ETHEREAL', + '3D_VOICE', + 'VIRTUAL_SURROUND', + 'CHORUS', + 'BATHROOM', + 'STAIRWELL', + 'NATURE', + 'UNCLE', + 'OLD_MAN', + 'BOY', + 'SISTER', + 'GIRL', + 'PIG_KING', + 'HULK', + 'RNB', + 'POPULAR', + 'PITCH_CORRECTION' + ] + private equalizationBands: Constants.AudioEqualizationBandFrequency[] = [ + Constants.AudioEqualizationBandFrequency.BAND_31, + Constants.AudioEqualizationBandFrequency.BAND_62, + Constants.AudioEqualizationBandFrequency.BAND_125, + Constants.AudioEqualizationBandFrequency.BAND_250, + Constants.AudioEqualizationBandFrequency.BAND_500, + Constants.AudioEqualizationBandFrequency.BAND_1K, + Constants.AudioEqualizationBandFrequency.BAND_2K, + Constants.AudioEqualizationBandFrequency.BAND_4K, + Constants.AudioEqualizationBandFrequency.BAND_8K, + Constants.AudioEqualizationBandFrequency.BAND_16K + ] + private equalizationBandNames: ResourceStr[] = [ + '31 Hz', + '62 Hz', + '125 Hz', + '250 Hz', + '500 Hz', + '1 kHz', + '2 kHz', + '4 kHz', + '8 kHz', + '16 kHz' + ] + private equalizationGains: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + private reverbKeys: Constants.AudioReverbType[] = [ + Constants.AudioReverbType.DRY_LEVEL, + Constants.AudioReverbType.WET_LEVEL, + Constants.AudioReverbType.ROOM_SIZE, + Constants.AudioReverbType.WET_DELAY, + Constants.AudioReverbType.STRENGTH + ] + private reverbKeyNames: ResourceStr[] = [ + 'DRY_LEVEL', + 'WET_LEVEL', + 'ROOM_SIZE', + 'WET_DELAY', + 'STRENGTH' + ] + private reverbValues: number[] = [0, 0, 0, 0, 0] + + @State channelName: string = '' + @State isJoin: boolean = false + @State localUid: number = 0 + @State remoteUid: number = 0 + @State voiceBeautifierIndex: number = 0 + @State audioEffectIndex: number = 0 + @State equalizationBandIndex: number = 0 + @State equalizationGain: number = 0 + @State reverbKeyIndex: number = 0 + @State reverbValue: number = 0 + + private showResult(api: string, value: string, result: number): void { + Logger.info(TAG, `${api} value:${value} result:${result}`) + if (result == Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(`${api}: ${value}`) + } else { + ShowToast.longToast(`${api} failed: ${result}`) + } + } + + private resetPresetSelections(engine: RtcEngine): void { + if (this.voiceBeautifierIndex != 0) { + engine.setVoiceBeautifierPreset(Constants.VoiceBeautifierPreset.OFF) + } + if (this.audioEffectIndex != 0) { + engine.setAudioEffectPreset(Constants.AudioEffectPreset.AUDIO_EFFECT_OFF) + } + this.voiceBeautifierIndex = 0 + this.audioEffectIndex = 0 + } + + private resetCustomEffectValues(): void { + this.equalizationGains = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + this.reverbValues = [0, 0, 0, 0, 0] + this.equalizationGain = 0 + this.reverbValue = 0 + } + + private getReverbMin(): number { + return this.reverbKeyIndex <= 1 ? -20 : 0 + } + + private getReverbMax(): number { + if (this.reverbKeyIndex <= 1) { + return 10 + } + if (this.reverbKeyIndex == 3) { + return 200 + } + return 100 + } + + aboutToAppear(): void { + const config = new RtcEngineConfig() + config.mAppId = AppID + config.mContext = getContext(this) as common.UIAbilityContext + config.mEventHandler = {} + config.mEventHandler.onJoinChannelSuccess = (cid: string, uid: number, elapsed: number) => { + Logger.info(TAG, `onJoinChannelSuccess uid:${uid}`) + this.isJoin = true + this.localUid = uid + } + config.mEventHandler.onLeaveChannel = () => { + Logger.info(TAG, 'onLeaveChannel') + this.isJoin = false + this.localUid = 0 + this.remoteUid = 0 + } + config.mEventHandler.onUserJoined = (uid: number, elapsed: number) => { + Logger.info(TAG, `onUserJoined uid:${uid} elapsed:${elapsed}`) + if (this.remoteUid == 0) { + this.remoteUid = uid + } + } + config.mEventHandler.onUserOffline = (uid: number, reason: number) => { + Logger.info(TAG, `onUserOffline uid:${uid} reason:${reason}`) + if (this.remoteUid == uid) { + this.remoteUid = 0 + } + } + config.mEventHandler.onError = (err: number, message: string) => { + Logger.error(TAG, `onError err:${err} message:${message}`) + ShowToast.longToast(`onError ${err}: ${message}`) + } + this.rtcEngine = RtcEngine.create(config) + this.rtcEngine.enableAudio() + } + + aboutToDisappear(): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + if (this.isJoin) { + engine.leaveChannel() + } + RtcEngine.destroy().then(() => { + Logger.info(TAG, 'destroy done') + }) + this.rtcEngine = undefined + } + + build() { + Column() { + TitleBar({ + title: $r('app.string.item_voiceeffects'), + showBack: true + }) + + if (this.isJoin) { + Row({ space: 8 }) { + Column() { + Text('Local') + Text(this.localUid.toString()) + } + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .backgroundColor($r('app.color.background_audio_cell')) + .layoutWeight(1) + .height(72) + + if (this.remoteUid != 0) { + Column() { + Text('Remote') + Text(this.remoteUid.toString()) + } + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .backgroundColor($r('app.color.background_audio_cell')) + .layoutWeight(1) + .height(72) + } + } + .width('100%') + .padding({ left: 12, right: 12, top: 8 }) + } + + Scroll() { + Column({ space: 10 }) { + Row() { + Text($r('app.string.voice_beautifier_preset')).layoutWeight(1) + Select(this.voiceBeautifierNames.map((name: ResourceStr) => { + const option: SelectOption = { value: name } + return option + })) + .selected(this.voiceBeautifierIndex) + .value(this.voiceBeautifierNames[this.voiceBeautifierIndex]) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('58%') + .optionWidth(280) + .optionHeight(320) + .onSelect((index: number, text: string) => { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.voiceBeautifierIndex = index + this.audioEffectIndex = 0 + this.resetCustomEffectValues() + engine.setAudioEffectPreset(Constants.AudioEffectPreset.AUDIO_EFFECT_OFF) + const result = engine.setVoiceBeautifierPreset(this.voiceBeautifierPresets[index]) + this.showResult('setVoiceBeautifierPreset', text, result) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.audio_effect_preset')).layoutWeight(1) + Select(this.audioEffectNames.map((name: ResourceStr) => { + const option: SelectOption = { value: name } + return option + })) + .selected(this.audioEffectIndex) + .value(this.audioEffectNames[this.audioEffectIndex]) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('58%') + .optionWidth(280) + .optionHeight(320) + .onSelect((index: number, text: string) => { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.audioEffectIndex = index + this.voiceBeautifierIndex = 0 + this.resetCustomEffectValues() + engine.setVoiceBeautifierPreset(Constants.VoiceBeautifierPreset.OFF) + const result = engine.setAudioEffectPreset(this.audioEffectPresets[index]) + this.showResult('setAudioEffectPreset', text, result) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Divider() + + Row() { + Text($r('app.string.equalization_band')).layoutWeight(1) + Select(this.equalizationBandNames.map((name: ResourceStr) => { + const option: SelectOption = { value: name } + return option + })) + .selected(this.equalizationBandIndex) + .value(this.equalizationBandNames[this.equalizationBandIndex]) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('50%') + .onSelect((index: number) => { + this.equalizationBandIndex = index + this.equalizationGain = this.equalizationGains[index] + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.equalization_gain')).layoutWeight(1) + Text(`${this.equalizationGain} dB`).width(52) + Slider({ value: this.equalizationGain, min: -15, max: 15, step: 1 }) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('48%') + .onChange((value: number, mode: SliderChangeMode) => { + this.equalizationGain = Math.round(value) + if (mode != SliderChangeMode.End && mode != SliderChangeMode.Click) { + return + } + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.equalizationGains[this.equalizationBandIndex] = this.equalizationGain + this.resetPresetSelections(engine) + const result = engine.setLocalVoiceEqualization( + this.equalizationBands[this.equalizationBandIndex], this.equalizationGain) + this.showResult('setLocalVoiceEqualization', + `${this.equalizationBandNames[this.equalizationBandIndex]} / ${this.equalizationGain} dB`, result) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Divider() + + Row() { + Text($r('app.string.reverb_key')).layoutWeight(1) + Select(this.reverbKeyNames.map((name: ResourceStr) => { + const option: SelectOption = { value: name } + return option + })) + .selected(this.reverbKeyIndex) + .value(this.reverbKeyNames[this.reverbKeyIndex]) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('50%') + .onSelect((index: number) => { + this.reverbKeyIndex = index + this.reverbValue = this.reverbValues[index] + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.reverb_value')).layoutWeight(1) + Text(this.reverbValue.toString()).width(44) + Slider({ + value: this.reverbValue, + min: this.getReverbMin(), + max: this.getReverbMax(), + step: 1 + }) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('48%') + .onChange((value: number, mode: SliderChangeMode) => { + this.reverbValue = Math.round(value) + if (mode != SliderChangeMode.End && mode != SliderChangeMode.Click) { + return + } + const engine = this.rtcEngine + if (engine == undefined) { + return + } + this.reverbValues[this.reverbKeyIndex] = this.reverbValue + this.resetPresetSelections(engine) + const result = engine.setLocalVoiceReverb( + this.reverbKeys[this.reverbKeyIndex], this.reverbValue) + this.showResult('setLocalVoiceReverb', + `${this.reverbKeyNames[this.reverbKeyIndex]} / ${this.reverbValue}`, result) + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + } + .width('100%') + .padding({ left: 12, right: 12, top: 12, bottom: 12 }) + } + .layoutWeight(1) + .width('100%') + .scrollBar(BarState.Auto) + + Row() { + TextInput({ placeholder: $r('app.string.text_input_channel_name') }) + .id('input_channel_name') + .enabled(!this.isJoin) + .enableKeyboardOnFocus(false) + .onChange((value: string) => { + this.channelName = value + }) + .layoutWeight(1) + .margin({ right: 6 }) + + Button(this.isJoin ? $r('app.string.leave') : $r('app.string.join')) + .id('button_to_join') + .onClick(async () => { + focusControl.requestFocus('button_to_join') + const engine = this.rtcEngine + if (engine == undefined) { + return + } + if (this.isJoin) { + engine.leaveChannel() + return + } + if (this.channelName == '') { + ShowToast.shortToast('The channel name is empty!') + return + } + if (!(await PermissionHelper.checkPermissions( + getContext(this) as common.UIAbilityContext, + ['ohos.permission.MICROPHONE'] + ))) { + ShowToast.shortToast('Permission leak!') + return + } + + engine.setAudioProfile(Constants.AudioProfileType.MUSIC_HIGH_QUALITY_STEREO) + engine.setAudioScenario(Constants.AudioScenarioType.GAME_STREAMING) + const mediaOptions = new ChannelMediaOptions() + mediaOptions.publishCameraTrack = false + mediaOptions.publishMicrophoneTrack = true + mediaOptions.autoSubscribeVideo = false + mediaOptions.autoSubscribeAudio = true + mediaOptions.channelProfile = Constants.ChannelProfile.LIVE_BROADCASTING + mediaOptions.clientRoleType = Constants.ClientRole.BROADCASTER + + const token = await TokenUtils.genRtcToken(this.channelName, '') + const result = engine.joinChannelWithOptions(token, this.channelName, 0, mediaOptions) + if (result != Constants.ErrorCode.ERR_OK) { + ShowToast.longToast(`joinChannelWithOptions failed: ${result} : ${RtcEngine.getErrorDescription(result)}`) + } + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_shallow_grey')) + } +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinAudioChannel.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinAudioChannel.ets index 02cdb295f..f854a60b2 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinAudioChannel.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinAudioChannel.ets @@ -45,6 +45,7 @@ struct JoinAudioChannel { @State remoteUidList: number[] = [] @State isMicophotoOpen: boolean = false @State isEarMonitorChecked: boolean = false + @State localVoicePitch: number = 1.0 private channelProfileType = Constants.ChannelProfile.COMMUNICATION private audioScenarioType = Constants.AudioScenarioType.DEFAULT private audioProfileType = Constants.AudioProfileType.DEFAULT @@ -100,6 +101,34 @@ struct JoinAudioChannel { Logger.info(TAG, `onChannelProfileSelect index ${index} text ${text}}`) this.channelProfileType = AudioConfigHelper.getChannelProfileType(text) } + private getAudioRouteName(route: Constants.AudioRouteTypes): string { + switch (route) { + case Constants.AudioRouteTypes.DEFAULT: + return 'DEFAULT' + case Constants.AudioRouteTypes.HEADSET: + return 'HEADSET' + case Constants.AudioRouteTypes.EARPIECE: + return 'EARPIECE' + case Constants.AudioRouteTypes.HEADSETNOMIC: + return 'HEADSETNOMIC' + case Constants.AudioRouteTypes.SPEAKERPHONE: + return 'SPEAKERPHONE' + case Constants.AudioRouteTypes.LOUDSPEAKER: + return 'LOUDSPEAKER' + case Constants.AudioRouteTypes.BLUETOOTH_DEVICE_HFP: + return 'BLUETOOTH_DEVICE_HFP' + case Constants.AudioRouteTypes.USBDEVICE: + return 'USBDEVICE' + case Constants.AudioRouteTypes.HDMI: + return 'HDMI' + case Constants.AudioRouteTypes.USB_HEADSET: + return 'USB_HEADSET' + case Constants.AudioRouteTypes.BLUETOOTH_DEVICE_A2DP: + return 'BLUETOOTH_DEVICE_A2DP' + default: + return `UNKNOWN(${route})` + } + } startServiceTask() { let wantAgentInfo: wantAgent.WantAgentInfo = { @@ -151,6 +180,10 @@ struct JoinAudioChannel { config.mEventHandler.onAudioVolumeIndication = (speakers: Array, totalVolume: number) => { Logger.info(TAG,`onAudioVolumeIndication totalVolume:${totalVolume} speakers:${JSON.stringify(speakers)}`) } + config.mEventHandler.onAudioRouteChanged = (route: Constants.AudioRouteTypes) => { + Logger.info(TAG, `mEventHandler.onAudioRouteChanged route:${route}`) + ShowToast.shortToast(`onAudioRouteChanged: ${this.getAudioRouteName(route)}`) + } config.mEventHandler.onUserJoined = (uid: number, collapse: number) => { Logger.info(TAG, "mEventHandler.onUserJoined: " + uid + " , " + collapse); this.remoteUidList.push(uid) @@ -305,72 +338,98 @@ struct JoinAudioChannel { } .align(Alignment.TopStart) - Column({ space: 5 }) { - Row() { - Text('') - Button((this.isJoin && !this.isMicophotoOpen) ? $r('app.string.open_microphone') : - $r('app.string.close_microphone')) - .onClick(this.handleMicospeakerClick) - .enabled(this.isJoin) - } - .width('100%') - .justifyContent(FlexAlign.SpaceBetween) + Scroll() { + Column({ space: 5 }) { + Row() { + Text('') + Button((this.isJoin && !this.isMicophotoOpen) ? $r('app.string.open_microphone') : + $r('app.string.close_microphone')) + .onClick(this.handleMicospeakerClick) + .enabled(this.isJoin) + } + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) - Row() { - Text($r('app.string.audio_router')) - AVCastPicker({ - normalColor: Color.Red, - customPicker: () => this.customPickerBuilder() + Row() { + Text($r('app.string.audio_router')) + AVCastPicker({ + normalColor: Color.Red, + customPicker: () => this.customPickerBuilder() + }) + .width(40) + .height(40) + } + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + + AudioChannelToggle({ + isEnable: this.isJoin, + name: $r('app.string.inear_monitor'), + isCheck: false, + onCheck: this.onInEarMonitorCheck + }) + AudioChannelSlider({ + isEnable: this.isJoin && this.isEarMonitorChecked, + name: $r('app.string.inear_monitor_vol'), + onSlide: this.onInEarMonitorVolSlide + }) + AudioChannelSlider({ + isEnable: this.isJoin, + name: $r('app.string.playout_vol'), + onSlide: this.onPlayoutVolSlide + }) + AudioChannelSlider({ + isEnable: this.isJoin, + name: $r('app.string.recording_vol'), + onSlide: this.onRecordingVolSlide + }) + AudioChannelSpinner({ + name: $r('app.string.scenario'), + items: AUDIO_SCENARIO, + onSelect: this.onScenarioSelect + }) + AudioChannelSpinner({ + // isEnable: !this.isJoin, + name: $r('app.string.audio_profile'), + items: AUDIO_PROFILE, + onSelect: this.onAudioProfileSelect + }) + Row() { + Text($r('app.string.voice_pitch')).layoutWeight(1) + Text(this.localVoicePitch.toFixed(1)).width(32) + Slider({ value: this.localVoicePitch, min: 0.5, max: 2.0, step: 0.1 }) + .enabled(this.isJoin) + .opacity(this.isJoin ? 1 : 0.4) + .width('50%') + .onChange((value: number, mode: SliderChangeMode) => { + this.localVoicePitch = value + let result = this.rtcEngine?.setLocalVoicePitch(value) + Logger.info(TAG, `setLocalVoicePitch pitch:${value} result:${result}`) + if (mode == SliderChangeMode.End || mode == SliderChangeMode.Click) { + if (result == Constants.ErrorCode.ERR_OK) { + ShowToast.shortToast(`Voice Pitch: ${value.toFixed(1)}`) + } else { + ShowToast.longToast(`setLocalVoicePitch failed: ${result}`) + } + } + }) + } + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + AudioChannelSpinner({ + isEnable: !this.isJoin, + name: $r('app.string.channel_profile'), + items: CHANNEL_PROFILE, + onSelect: this.onChannelProfileSelect }) - .width(40) - .height(40) } + .alignItems(HorizontalAlign.End) .width('100%') - .justifyContent(FlexAlign.SpaceBetween) - - AudioChannelToggle({ - isEnable: this.isJoin, - name: $r('app.string.inear_monitor'), - isCheck: false, - onCheck: this.onInEarMonitorCheck - }) - AudioChannelSlider({ - isEnable: this.isJoin && this.isEarMonitorChecked, - name: $r('app.string.inear_monitor_vol'), - onSlide: this.onInEarMonitorVolSlide - }) - AudioChannelSlider({ - isEnable: this.isJoin, - name: $r('app.string.playout_vol'), - onSlide: this.onPlayoutVolSlide - }) - AudioChannelSlider({ - isEnable: this.isJoin, - name: $r('app.string.recording_vol'), - onSlide: this.onRecordingVolSlide - }) - AudioChannelSpinner({ - name: $r('app.string.scenario'), - items: AUDIO_SCENARIO, - onSelect: this.onScenarioSelect - }) - AudioChannelSpinner({ - // isEnable: !this.isJoin, - name: $r('app.string.audio_profile'), - items: AUDIO_PROFILE, - onSelect: this.onAudioProfileSelect - }) - AudioChannelSpinner({ - isEnable: !this.isJoin, - name: $r('app.string.channel_profile'), - items: CHANNEL_PROFILE, - onSelect: this.onChannelProfileSelect - }) } .layoutWeight(1) .margin({ top: 10, bottom: 10 }) - .justifyContent(FlexAlign.Center) - .alignItems(HorizontalAlign.End) + .scrollBar(BarState.Auto) .width('100%') Row() { @@ -436,4 +495,4 @@ struct JoinAudioChannel { .height('100%') .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinVideoChannel.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinVideoChannel.ets index 451216568..dfddaca52 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinVideoChannel.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/pages/basic/JoinVideoChannel.ets @@ -15,12 +15,25 @@ const TAG: string = 'JoinVideoChannel' @Component struct JoinVideoChannel { private rtcEngine: RtcEngine | undefined = undefined; + private localXComponentController: XComponentController = new XComponentController() + private remoteXComponentController: XComponentController = new XComponentController() @State message: string = 'Hello World'; @State channelName: string = '' @State isJoin: boolean = false @State localUid: number = 0 @State remoteUid: number = 0 + private getNtpWallTime(): void { + const engine = this.rtcEngine + if (engine == undefined) { + return + } + const ntpWallTime: bigint = engine.getNtpWallTimeInMs() + const ntpWallTimeInMs: string = ntpWallTime.toString() + Logger.info(TAG, `getNtpWallTimeInMs result:${ntpWallTimeInMs}`) + ShowToast.shortToast(`getNtpWallTimeInMs: ${ntpWallTimeInMs} ms`) + } + aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear') let config: RtcEngineConfig = new RtcEngineConfig(); @@ -94,9 +107,10 @@ struct JoinVideoChannel { XComponent({ id: 'preview_local', type: XComponentType.SURFACE, - libraryname: Constants.AGORA_LIB_NAME, + controller: this.localXComponentController, }).onLoad(() => { - let localCanvas = new VideoCanvas("preview_local"); + const surfaceId = this.localXComponentController.getXComponentSurfaceId() + let localCanvas = new VideoCanvas(surfaceId, true); localCanvas.uid = this.localUid; localCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN; localCanvas.mirrorMode = 0; @@ -108,13 +122,14 @@ struct JoinVideoChannel { XComponent({ id: 'preview_remote', type: XComponentType.SURFACE, - libraryname: Constants.AGORA_LIB_NAME, + controller: this.remoteXComponentController, }).onLoad(() => { - let localCanvas = new VideoCanvas("preview_remote"); - localCanvas.uid = this.remoteUid; - localCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN; - localCanvas.mirrorMode = 0; - this.rtcEngine?.setupRemoteVideo(localCanvas); + const surfaceId = this.remoteXComponentController.getXComponentSurfaceId() + let remoteCanvas = new VideoCanvas(surfaceId, true); + remoteCanvas.uid = this.remoteUid; + remoteCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN; + remoteCanvas.mirrorMode = 0; + this.rtcEngine?.setupRemoteVideo(remoteCanvas); }).width('50%') } } @@ -125,6 +140,13 @@ struct JoinVideoChannel { .align(Alignment.TopStart) .flexGrow(1) + Button($r('app.string.get_ntp_time')) + .onClick(() => { + this.getNtpWallTime() + }) + .alignSelf(ItemAlign.End) + .margin({ top: 6, right: 12, bottom: 6 }) + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { TextInput({ placeholder: $r('app.string.text_input_channel_name') }) .id("input_channel_name") @@ -187,4 +209,4 @@ struct JoinVideoChannel { } .backgroundColor($r('app.color.background_shallow_grey')) } -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/AudioConfigContant.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/AudioConfigContant.ets index 9217c9b0d..c4533d875 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/AudioConfigContant.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/AudioConfigContant.ets @@ -17,20 +17,20 @@ export class AudioConfigHelper { // public static readonly CHANNEL_PROFILE_CLOUD_GAMING = 'CLOUD_GAMING' // public static readonly CHANNEL_PROFILE_COMMUNICATION_1v1 = 'COMMUNICATION_1v1' - public static getAudioRouteType(position: number) { - let type = Constants.AudioRouteTypes.AUDIO_ROUTE_EARPIECE + public static getAudioRouteType(position: number): Constants.AudioRouteTypes { + let type: Constants.AudioRouteTypes = Constants.AudioRouteTypes.EARPIECE switch (position) { case 0: - type = Constants.AudioRouteTypes.AUDIO_ROUTE_SPEAKERPHONE + type = Constants.AudioRouteTypes.SPEAKERPHONE break case 1: - type = Constants.AudioRouteTypes.AUDIO_ROUTE_EARPIECE + type = Constants.AudioRouteTypes.EARPIECE break case 2: - type = Constants.AudioRouteTypes.AUDIO_ROUTE_HEADSET + type = Constants.AudioRouteTypes.HEADSET break case 3: - type = Constants.AudioRouteTypes.AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP + type = Constants.AudioRouteTypes.BLUETOOTH_DEVICE_HFP break } return type @@ -107,4 +107,4 @@ export const AUDIO_PROFILE = [ export const CHANNEL_PROFILE = [ AudioConfigHelper.CHANNEL_PROFILE_COMMUNICATION, AudioConfigHelper.CHANNEL_PROFILE_LIVE_BROADCASTING -] \ No newline at end of file +] diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/TokenUtils.ets b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/TokenUtils.ets index 16bfe16ad..569d3146e 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/TokenUtils.ets +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/ets/util/TokenUtils.ets @@ -6,21 +6,21 @@ import Logger from './Logger'; export class TokenUtils { static async genRtcToken(channelName: string, uid: string) : Promise { - const session = rcp.createSession({ - headers: { 'Content-Type': 'application/json' } - }); - let body = JSON.stringify({ - "appId": AppID, - "appCertificate": AppCertificate, - "channelName": channelName, - "expire": 900, //s - "src": "HMOS", - "ts": systemDateTime.getTime(false), - "type": 1, // 1: RTC Token ; 2: RTM Token - "uid": uid - }) let token = '' try{ + const session = rcp.createSession({ + headers: { 'Content-Type': 'application/json' } + }); + let body = JSON.stringify({ + "appId": AppID, + "appCertificate": AppCertificate, + "channelName": channelName, + "expire": 900, //s + "src": "HMOS", + "ts": systemDateTime.getTime(false), + "type": 1, // 1: RTC Token ; 2: RTM Token + "uid": uid + }) let response = await session.post("https://toolbox.bj2.agoralab.co/v1/token/generate", body) Logger.info(`Response complete: statueCode=${response.statusCode}`); let decoder = util.TextDecoder.create('utf-8'); 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 968b2adf4..df4eed4dc 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 @@ -351,6 +351,158 @@ { "name": "click_select_router", "value": "Click Select Router" + }, + { + "name": "voice_pitch", + "value": "Voice Pitch" + }, + { + "name": "all_audio_effects", + "value": "All Audio Effects" + }, + { + "name": "play_all", + "value": "Play All" + }, + { + "name": "resume_all", + "value": "Resume All" + }, + { + "name": "pause_all", + "value": "Pause All" + }, + { + "name": "stop_all", + "value": "Stop All" + }, + { + "name": "unload_all", + "value": "Unload All" + }, + { + "name": "remote_subscribe_fallback", + "value": "Remote Subscribe Fallback" + }, + { + "name": "fallback_disabled", + "value": "Disabled" + }, + { + "name": "fallback_low_stream", + "value": "Low Stream" + }, + { + "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" + }, + { + "name": "get_ntp_time", + "value": "Get NTP Time" + }, + { + "name": "item_useraccount", + "value": "Join with User Account" + }, + { + "name": "text_input_user_account", + "value": "Please input user account" + }, + { + "name": "mapped_int_uid", + "value": "Mapped Int UID" + }, + { + "name": "register_user_account", + "value": "Register Account" + }, + { + "name": "preload_user_account_channel", + "value": "Preload Channel" + }, + { + "name": "channel_name_empty", + "value": "The channel name is empty!" + }, + { + "name": "user_account_empty", + "value": "The user account is empty!" + }, + { + "name": "item_voiceeffects", + "value": "Voice Effects" + }, + { + "name": "voice_beautifier_preset", + "value": "Voice Beautifier" + }, + { + "name": "audio_effect_preset", + "value": "Audio Effect Preset" + }, + { + "name": "equalization_band", + "value": "Equalization Band" + }, + { + "name": "equalization_gain", + "value": "Equalization Gain" + }, + { + "name": "reverb_key", + "value": "Reverb Key" + }, + { + "name": "reverb_value", + "value": "Reverb Value" + }, + { + "name": "item_videobeauty", + "value": "Video Beauty" + }, + { + "name": "beauty_effect", + "value": "Beauty Effect" + }, + { + "name": "beauty_contrast", + "value": "Lightening Contrast" + }, + { + "name": "beauty_contrast_low", + "value": "Low" + }, + { + "name": "beauty_contrast_normal", + "value": "Normal" + }, + { + "name": "beauty_contrast_high", + "value": "High" + }, + { + "name": "beauty_lightening", + "value": "Lightening" + }, + { + "name": "beauty_smoothness", + "value": "Smoothness" + }, + { + "name": "beauty_redness", + "value": "Redness" + }, + { + "name": "beauty_sharpness", + "value": "Sharpness" } ] -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/profile/main_pages.json b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/profile/main_pages.json index 138f41c65..b3bfab3f8 100644 --- a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/profile/main_pages.json +++ b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/base/profile/main_pages.json @@ -7,7 +7,10 @@ "pages/basic/JoinAudioChannel", "pages/advance/LiveStreaming", "pages/advance/MediaMetadataWrap", + "pages/advance/VoiceEffects", + "pages/advance/VideoBeauty", "pages/advance/JoinMultiChannel", + "pages/advance/UserAccount", "pages/advance/CustomAudioSource", "pages/advance/CustomVideoSource", "pages/advance/OriginVideoDataWrap", 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 d0b8fef0e..cad7f7418 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 @@ -351,6 +351,158 @@ { "name": "click_select_router", "value": "Click Select Router" + }, + { + "name": "voice_pitch", + "value": "Voice Pitch" + }, + { + "name": "all_audio_effects", + "value": "All Audio Effects" + }, + { + "name": "play_all", + "value": "Play All" + }, + { + "name": "resume_all", + "value": "Resume All" + }, + { + "name": "pause_all", + "value": "Pause All" + }, + { + "name": "stop_all", + "value": "Stop All" + }, + { + "name": "unload_all", + "value": "Unload All" + }, + { + "name": "remote_subscribe_fallback", + "value": "Remote Subscribe Fallback" + }, + { + "name": "fallback_disabled", + "value": "Disabled" + }, + { + "name": "fallback_low_stream", + "value": "Low Stream" + }, + { + "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" + }, + { + "name": "get_ntp_time", + "value": "Get NTP Time" + }, + { + "name": "item_useraccount", + "value": "Join with User Account" + }, + { + "name": "text_input_user_account", + "value": "Please input user account" + }, + { + "name": "mapped_int_uid", + "value": "Mapped Int UID" + }, + { + "name": "register_user_account", + "value": "Register Account" + }, + { + "name": "preload_user_account_channel", + "value": "Preload Channel" + }, + { + "name": "channel_name_empty", + "value": "The channel name is empty!" + }, + { + "name": "user_account_empty", + "value": "The user account is empty!" + }, + { + "name": "item_voiceeffects", + "value": "Voice Effects" + }, + { + "name": "voice_beautifier_preset", + "value": "Voice Beautifier" + }, + { + "name": "audio_effect_preset", + "value": "Audio Effect Preset" + }, + { + "name": "equalization_band", + "value": "Equalization Band" + }, + { + "name": "equalization_gain", + "value": "Equalization Gain" + }, + { + "name": "reverb_key", + "value": "Reverb Key" + }, + { + "name": "reverb_value", + "value": "Reverb Value" + }, + { + "name": "item_videobeauty", + "value": "Video Beauty" + }, + { + "name": "beauty_effect", + "value": "Beauty Effect" + }, + { + "name": "beauty_contrast", + "value": "Lightening Contrast" + }, + { + "name": "beauty_contrast_low", + "value": "Low" + }, + { + "name": "beauty_contrast_normal", + "value": "Normal" + }, + { + "name": "beauty_contrast_high", + "value": "High" + }, + { + "name": "beauty_lightening", + "value": "Lightening" + }, + { + "name": "beauty_smoothness", + "value": "Smoothness" + }, + { + "name": "beauty_redness", + "value": "Redness" + }, + { + "name": "beauty_sharpness", + "value": "Sharpness" } ] -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/rawfile/bg_blue.png b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/rawfile/bg_blue.png new file mode 100644 index 000000000..4b1fa73e1 Binary files /dev/null and b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/rawfile/bg_blue.png differ diff --git a/HarmonyOS_NEXT/APIExample/entry/src/main/resources/rawfile/effectA.wav b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/rawfile/effectA.wav new file mode 100644 index 000000000..dc31fdb68 Binary files /dev/null and b/HarmonyOS_NEXT/APIExample/entry/src/main/resources/rawfile/effectA.wav differ 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 b617b8739..d535e55ae 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 @@ -218,7 +218,7 @@ }, { "name": "audio_effect", - "value": "音效" + "value": "单个音效文件" }, { "name": "start", @@ -351,6 +351,158 @@ { "name": "click_select_router", "value": "点击选择路由" + }, + { + "name": "voice_pitch", + "value": "人声音调" + }, + { + "name": "all_audio_effects", + "value": "全部音效文件" + }, + { + "name": "play_all", + "value": "全部播放" + }, + { + "name": "resume_all", + "value": "全部恢复" + }, + { + "name": "pause_all", + "value": "全部暂停" + }, + { + "name": "stop_all", + "value": "全部停止" + }, + { + "name": "unload_all", + "value": "全部卸载" + }, + { + "name": "remote_subscribe_fallback", + "value": "远端订阅降级" + }, + { + "name": "fallback_disabled", + "value": "关闭" + }, + { + "name": "fallback_low_stream", + "value": "降级到小流" + }, + { + "name": "fallback_audio_only", + "value": "降级到纯音频" + }, + { + "name": "remote_user_high_priority", + "value": "远端用户高优先级" + }, + { + "name": "video_image_source", + "value": "图片视频轨" + }, + { + "name": "get_ntp_time", + "value": "获取 NTP 时间" + }, + { + "name": "item_useraccount", + "value": "User Account 入会" + }, + { + "name": "text_input_user_account", + "value": "请输入用户账号" + }, + { + "name": "mapped_int_uid", + "value": "映射 Int UID" + }, + { + "name": "register_user_account", + "value": "注册账号" + }, + { + "name": "preload_user_account_channel", + "value": "预加载频道" + }, + { + "name": "channel_name_empty", + "value": "频道名不能为空" + }, + { + "name": "user_account_empty", + "value": "用户账号不能为空" + }, + { + "name": "item_voiceeffects", + "value": "美声与音效" + }, + { + "name": "voice_beautifier_preset", + "value": "美声预设" + }, + { + "name": "audio_effect_preset", + "value": "人声音效预设" + }, + { + "name": "equalization_band", + "value": "均衡频段" + }, + { + "name": "equalization_gain", + "value": "均衡增益" + }, + { + "name": "reverb_key", + "value": "混响参数" + }, + { + "name": "reverb_value", + "value": "混响数值" + }, + { + "name": "item_videobeauty", + "value": "视频美颜" + }, + { + "name": "beauty_effect", + "value": "美颜" + }, + { + "name": "beauty_contrast", + "value": "美白对比度" + }, + { + "name": "beauty_contrast_low", + "value": "低" + }, + { + "name": "beauty_contrast_normal", + "value": "正常" + }, + { + "name": "beauty_contrast_high", + "value": "高" + }, + { + "name": "beauty_lightening", + "value": "美白" + }, + { + "name": "beauty_smoothness", + "value": "平滑" + }, + { + "name": "beauty_redness", + "value": "红润" + }, + { + "name": "beauty_sharpness", + "value": "锐利" } ] -} \ No newline at end of file +} diff --git a/HarmonyOS_NEXT/APIExample/oh-package-lock.json5 b/HarmonyOS_NEXT/APIExample/oh-package-lock.json5 index 3f676bb1f..9dd55d341 100644 --- a/HarmonyOS_NEXT/APIExample/oh-package-lock.json5 +++ b/HarmonyOS_NEXT/APIExample/oh-package-lock.json5 @@ -1,6 +1,7 @@ { "meta": { - "stableOrder": true + "stableOrder": true, + "enableUnifiedLockfile": false }, "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",