diff --git a/ApiExample/.gitignore b/ApiExample/.gitignore new file mode 100644 index 0000000..d2ff201 --- /dev/null +++ b/ApiExample/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/ApiExample/AppScope/app.json5 b/ApiExample/AppScope/app.json5 new file mode 100644 index 0000000..834425c --- /dev/null +++ b/ApiExample/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.qiniu.apiexample", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/ApiExample/AppScope/resources/base/element/string.json b/ApiExample/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..be9aa24 --- /dev/null +++ b/ApiExample/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "ApiExample" + } + ] +} diff --git a/ApiExample/AppScope/resources/base/media/app_icon.png b/ApiExample/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000..a39445d Binary files /dev/null and b/ApiExample/AppScope/resources/base/media/app_icon.png differ diff --git a/ApiExample/build-profile.json5 b/ApiExample/build-profile.json5 new file mode 100644 index 0000000..2344ca0 --- /dev/null +++ b/ApiExample/build-profile.json5 @@ -0,0 +1,40 @@ +{ + "app": { + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.0.0(12)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug" + }, + { + "name": "release" + } + ], + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/ApiExample/code-linter.json5 b/ApiExample/code-linter.json5 new file mode 100644 index 0000000..77b31b5 --- /dev/null +++ b/ApiExample/code-linter.json5 @@ -0,0 +1,20 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + } +} \ No newline at end of file diff --git a/ApiExample/entry/.gitignore b/ApiExample/entry/.gitignore new file mode 100644 index 0000000..e2713a2 --- /dev/null +++ b/ApiExample/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/ApiExample/entry/build-profile.json5 b/ApiExample/entry/build-profile.json5 new file mode 100644 index 0000000..dfb34f7 --- /dev/null +++ b/ApiExample/entry/build-profile.json5 @@ -0,0 +1,28 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/ApiExample/entry/hvigorfile.ts b/ApiExample/entry/hvigorfile.ts new file mode 100644 index 0000000..c6edcd9 --- /dev/null +++ b/ApiExample/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/ApiExample/entry/obfuscation-rules.txt b/ApiExample/entry/obfuscation-rules.txt new file mode 100644 index 0000000..272efb6 --- /dev/null +++ b/ApiExample/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/ApiExample/entry/oh-package-lock.json5 b/ApiExample/entry/oh-package-lock.json5 new file mode 100644 index 0000000..0b83b05 --- /dev/null +++ b/ApiExample/entry/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "libqnrtckit.so@../oh_modules/.ohpm/qnrtckit@jtqfpmkvxl5mg0cnonjs8tibca55lsdycdg9p3sqg1k=/oh_modules/qnrtckit/src/main/cpp/types/libqnrtckit": "libqnrtckit.so@../oh_modules/.ohpm/qnrtckit@jtqfpmkvxl5mg0cnonjs8tibca55lsdycdg9p3sqg1k=/oh_modules/qnrtckit/src/main/cpp/types/libqnrtckit", + "qnrtckit@library/qnrtckit.har": "qnrtckit@library/qnrtckit.har" + }, + "packages": { + "libqnrtckit.so@../oh_modules/.ohpm/qnrtckit@jtqfpmkvxl5mg0cnonjs8tibca55lsdycdg9p3sqg1k=/oh_modules/qnrtckit/src/main/cpp/types/libqnrtckit": { + "name": "libqnrtckit.so", + "version": "1.0.0", + "resolved": "../oh_modules/.ohpm/qnrtckit@jtqfpmkvxl5mg0cnonjs8tibca55lsdycdg9p3sqg1k=/oh_modules/qnrtckit/src/main/cpp/types/libqnrtckit", + "registryType": "local" + }, + "qnrtckit@library/qnrtckit.har": { + "name": "qnrtckit", + "version": "1.0.0", + "resolved": "library/qnrtckit.har", + "registryType": "local", + "dependencies": { + "libqnrtckit.so": "file:./src/main/cpp/types/libqnrtckit" + } + } + } +} \ No newline at end of file diff --git a/ApiExample/entry/oh-package.json5 b/ApiExample/entry/oh-package.json5 new file mode 100644 index 0000000..a26b6cc --- /dev/null +++ b/ApiExample/entry/oh-package.json5 @@ -0,0 +1,13 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "qnrtckit": "file:library/qnrtckit.har" + }, + "devDependencies": {}, + "dynamicDependencies": {} +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/ets/entryability/EntryAbility.ets b/ApiExample/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..58056ae --- /dev/null +++ b/ApiExample/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,41 @@ +import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); + } +} diff --git a/ApiExample/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/ApiExample/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000..b72fc47 --- /dev/null +++ b/ApiExample/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,12 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(0x0000, 'testTag', 'onBackup ok'); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(0x0000, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + } +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/ets/pages/Index.ets b/ApiExample/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..fb9f95d --- /dev/null +++ b/ApiExample/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,171 @@ +import { scanCore, scanBarcode } from '@kit.ScanKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { CameraVideoExample } from './video/cameraVideo/CameraVideoExample'; +import { abilityAccessCtrl, common, Permissions, UIAbility } from '@kit.AbilityKit'; +import prompt from '@ohos.prompt' + +class DividerList { + strokeWidth: Length + startMargin: Length + endMargin: Length + color: ResourceColor + + constructor(strokeWidth: Length, startMargin: Length, endMargin: Length, color: ResourceColor) { + this.strokeWidth = strokeWidth + this.startMargin = startMargin + this.endMargin = endMargin + this.color = color + } +} + +const permissions: Array = + ['ohos.permission.MICROPHONE', 'ohos.permission.CAMERA', 'ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY', + 'ohos.permission.INTERNET']; + +function reqPermissionsFromUser(permissions: Array, context: common.UIAbilityContext): void { + let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); + // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗 + atManager.requestPermissionsFromUser(context, permissions).then((data) => { + let grantStatus: Array = data.authResults; + let length: number = grantStatus.length; + for (let i = 0; i < length; i++) { + if (grantStatus[i] === 0) { + // 用户授权,可以继续访问目标操作 + } else { + // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限 + return; + } + } + // 授权成功 + }).catch((err: BusinessError) => { + console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`); + }) +} + +@Entry +@Component +struct Index { + @Provide('pathStack') pathStack: NavPathStack = new NavPathStack(); + itemTitleSize: number = 15 + titleBackgroundColor: ResourceColor = '#fff0f0f0' + egDivider: DividerList = new DividerList(1, 10, 10, '#ffd0d0d0') + private pushClicked : boolean = false; + token: string = "" + + aboutToAppear(): void { + const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; + reqPermissionsFromUser(permissions, context); + this.pathStack.setInterception({ + didShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar", + operation: NavigationOperation, animated: boolean) => { + if (operation == NavigationOperation.POP) { + this.pushClicked = false; + } + } + }) + } + + @Builder + GroupHead(text: string) { + Text(text) + .fontSize(this.itemTitleSize) + .backgroundColor(this.titleBackgroundColor) + .width('100%') + .height(40) + .padding(5) + } + + @Builder + ListItemFunc(title: string, pathName: string) { + ListItem() { + Row() { + Column() { + Text(title).fontSize(this.itemTitleSize).alignSelf(ItemAlign.Start).width('100%').height('100%') + }.width('100%').height(40) + } + }.padding(5) + .onClick(() => { + if (this.pushClicked){ + return; + } + if (this.token) { + this.pushClicked = true; + this.pathStack.pushPathByName(pathName, this.token); + } else { + prompt.showToast({ + message: 'Token 为空,请点击扫描获取 Token', + duration: 2000 + }); + } + }) + } + + @Builder + PageMap(name: string) { + NavDestination() { + if (name == "CameraVideoExample") { + CameraVideoExample() + } + } + } + + @Builder + NavigationMenus() { + Row() { + Image('resources/base/media/scan.png') + .width(48) + .height(48) + .padding(10) + .onClick(() => { + // 定义扫码参数options + let options: scanBarcode.ScanOptions = { + scanTypes: [scanCore.ScanType.ALL], + enableMultiMode: true, + enableAlbum: true + }; + try { + // 可调用getContext接口获取当前页面关联的UIAbilityContext + scanBarcode.startScanForResult(getContext(this), options).then((result: scanBarcode.ScanResult) => { + // 解析码值结果跳转应用服务页 + console.info(`Succeeded in getting ScanResult by promise with options, result is ${result.originalValue}`); + if (result.originalValue) { + this.token = result.originalValue + prompt.showToast({ + message: '获取Token成功,请点击进入相关页面', + duration: 2000 + }); + } else { + prompt.showToast({ + message: '获取Token失败', + duration: 2000 + }); + } + }).catch((error: BusinessError) => { + console.error(`Failed to get ScanResult by promise with options. Code:${error.code}, message: ${error.message}`); + }); + } catch (error) { + console.error(`Failed to start the scanning service. Code:${error.code}, message: ${error.message}`); + } + }) + } + } + + build() { + Navigation(this.pathStack) { + List() { + ListItemGroup({ header: this.GroupHead('视频通话相关') }) { + this.ListItemFunc('摄像头采集音视频通话', + 'CameraVideoExample') + }.margin({ bottom: 5 }).divider(this.egDivider) + } + .sticky(StickyStyle.Header) + .scrollBar(BarState.Auto) + .height('95%') + } + .navDestination(this.PageMap) + .hideTitleBar(false) + .title("ApiExample") + .menus(this.NavigationMenus) + } +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/ets/pages/video/cameraVideo/CameraVideoExample.ets b/ApiExample/entry/src/main/ets/pages/video/cameraVideo/CameraVideoExample.ets new file mode 100644 index 0000000..7b49292 --- /dev/null +++ b/ApiExample/entry/src/main/ets/pages/video/cameraVideo/CameraVideoExample.ets @@ -0,0 +1,211 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; + +import { + QNRTC, + QNRTCSetting, + QNLogConfig, + QNMicrophoneAudioTrackConfig, + QNRTCClientConfig, + QNRTCClient, + QNMicrophoneAudioTrack, + QNLocalTrack, + QNCameraVideoTrack, + QNRemoteVideoTrack, + QNCameraVideoTrackConfig, + QNRemoteAudioTrack, + QNRemoteTrack, + QNClientEventListener, + QNConnectionState, + QNConnectionDisconnectedInfo, + QNVideoCaptureConfig, + QNVideoEncoderConfig, + QNComponentController +} from 'qnrtckit' + +@Styles +function paddingStyle() { + .width('100%') + .height('100%') + .aspectRatio(9.0 / 16) + .borderRadius(5) + .backgroundColor(Color.White) +} + +@Entry +@Component +export struct CameraVideoExample { + @Consume('pathStack') pathInfos: NavPathStack; + private token: string = "" + private client: QNRTCClient | null = null; + private micTrack: QNMicrophoneAudioTrack | null = null; + private localTracks: QNLocalTrack[] = []; + private cameraTrack: QNCameraVideoTrack | null = null; + private localRenderComponentCtrl: QNComponentController = new QNComponentController(); + private remoteRenderComponentCtrl: QNComponentController = new QNComponentController(); + private connectionState: QNConnectionState = QNConnectionState.Disconnected; + @State localRendervisibility: Visibility = Visibility.Hidden; + @State remoteRendervisibility: Visibility = Visibility.Hidden; + private clientListener: QNClientEventListener = { + OnConnectionStateChanged: (state: QNConnectionState, info: QNConnectionDisconnectedInfo) => { + this.connectionState = state; + if (state == QNConnectionState.Connected) { + // set local render + this.cameraTrack!.Play(this.localRenderComponentCtrl); + this.localRendervisibility = Visibility.Visible; + // publish local tracks + if (this.client) { + this.client.Publish(this.localTracks, null); + } + } + }, + OnSubscribed: (userid: string, remoteAudioTrackList: QNRemoteAudioTrack[], + remoteVideoTrackList: QNRemoteVideoTrack[]) => { + if (remoteVideoTrackList.length) { + let remoteVideoTrack: QNRemoteVideoTrack = remoteVideoTrackList[0] + // set remote render + remoteVideoTrack.Play(this.remoteRenderComponentCtrl); + this.remoteRendervisibility = Visibility.Visible; + } + }, + OnUserJoined: (remoteUserId: string, userData: string) => { + console.log("OnUserJoined: ", remoteUserId); + }, + OnUserLeft: (remoteUserId: string) => { + console.log("OnUserLeft: ", remoteUserId); + }, + OnUserReconnecting: (remoteUserId: string) => { + console.log("OnUserReconnecting: ", remoteUserId); + }, + OnUserReconnected: (remoteUserId: string) => { + console.log("OnUserReconnected: ", remoteUserId); + }, + OnUserPublished: (remoteUserId: string, remoteTrackList: QNRemoteTrack[]) => { + console.log("OnUserPublished: ", remoteUserId); + }, + OnUserUnpublished: (remoteUserId: string, remoteTrackList: QNRemoteTrack[]) => { + console.log("OnUserUnpublished: ", remoteUserId); + } + } + + aboutToAppear(): void { + this.initRtcClient(); + this.join(); + } + + aboutToDisappear(): void { + if (this.client && this.connectionState == QNConnectionState.Connected) { + this.client!.Leave(); + } + QNRTC.DeInit(); + } + + initRtcClient(): void { + // set log + let logConfig = new QNLogConfig(); + logConfig.dir = getContext(this).filesDir; + logConfig.logLevel = 2; + QNRTC.SetLogConfig(logConfig); + // init rtc + let setting = new QNRTCSetting(); + let ret = QNRTC.Init(setting); + // create rtc client + this.client = QNRTC.CreateClient(new QNRTCClientConfig()) + this.client!.SetClientEventListener(this.clientListener); + // create track + this.micTrack = QNRTC.CreateMicrophoneAudioTrack(new QNMicrophoneAudioTrackConfig()); + let videoTrackConfig = new QNCameraVideoTrackConfig() + let videoCaptureConfig = new QNVideoCaptureConfig() + let videoEncodeConfig = new QNVideoEncoderConfig() + videoTrackConfig.encoderConfig = videoEncodeConfig; + videoTrackConfig.captureConfig = videoCaptureConfig; + this.cameraTrack = QNRTC.CreateCameraVideoTrack(videoTrackConfig) + // set local track + if (this.micTrack) { + this.localTracks.push(this.micTrack); + } + if (this.cameraTrack) { + this.localTracks.push(this.cameraTrack); + } + } + + join(): void { + // get token + let styledString: MutableStyledString = new MutableStyledString(JSON.stringify(this.pathInfos.getParamByIndex(0))) + styledString.removeString(0, 1); + styledString.removeString(styledString.length - 1, 1); + this.token = styledString.getString(); + // join room + this.client!.Join(this.token); + } + + build() { + NavDestination() { + Column() { + Row() { + Stack() { + Text("本地视图") + .textAlign(TextAlign.Center) + .fontColor(Color.Black) + .align(Alignment.Center) + .fontSize(18) + .width('100%') + .height('100%') + + XComponent({ + type: XComponentType.SURFACE, + controller: this.localRenderComponentCtrl + }).onLoad(() => { + console.log("local surfaceid", this.localRenderComponentCtrl.getXComponentSurfaceId()); + }).paddingStyle().align(Alignment.Center).visibility(this.localRendervisibility) + } + .width('50%') + .height('50%') + .backgroundColor(Color.Grey) + + Stack() { + Text("远端视图") + .textAlign(TextAlign.Center) + .fontColor(Color.Black) + .align(Alignment.Center) + .fontSize(18) + .width('100%') + .height('100%') + + XComponent({ + type: XComponentType.SURFACE, + controller: this.remoteRenderComponentCtrl + }).onLoad(() => { + console.log("remote surfaceid: ", this.remoteRenderComponentCtrl.getXComponentSurfaceId()); + }).paddingStyle().align(Alignment.Center).visibility(this.remoteRendervisibility) + } + .width('50%') + .height('50%') + .backgroundColor(Color.Grey) + } + + Row() { + // todo + } + .width('100%') + .height('30%') + .padding(5) + .justifyContent(FlexAlign.Center) + .backgroundColor(Color.Grey) + .borderRadius(5) + + Row() { + Text("Tips: 本示例仅展示一对一场景下 SDK 内置摄像头采集视频 Track 和麦克风采集音频 Track 的发布和订阅") + .textAlign(TextAlign.Start) + .fontColor(Color.Black) + .align(Alignment.Bottom) + .fontSize(13) + .padding(3) + } + .width('100%') + .height('15%') + .padding(5) + .justifyContent(FlexAlign.End) + } + } + } +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/module.json5 b/ApiExample/entry/src/main/module.json5 new file mode 100644 index 0000000..7472bea --- /dev/null +++ b/ApiExample/entry/src/main/module.json5 @@ -0,0 +1,88 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY", + "reason": "$string:file_reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + }, + }, + { + "name": "ohos.permission.INTERNET", + "reason": "$string:network", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, { + "name": "ohos.permission.MICROPHONE", + "reason": "$string:mic_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, { + "name": "ohos.permission.CAMERA", + "reason": "$string:mic_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/resources/base/element/color.json b/ApiExample/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..d66f9a7 --- /dev/null +++ b/ApiExample/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/resources/base/element/string.json b/ApiExample/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..646870b --- /dev/null +++ b/ApiExample/entry/src/main/resources/base/element/string.json @@ -0,0 +1,32 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ApiExample" + }, + { + "name": "file_reason", + "value": "for log collection" + }, + { + "name": "network", + "value": "request network" + }, + { + "name": "mic_reason", + "value": "for audio collection" + }, + { + "name": "camera_reason", + "value": "for video collection" + } + ] +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/resources/base/media/background.png b/ApiExample/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000..f939c9f Binary files /dev/null and b/ApiExample/entry/src/main/resources/base/media/background.png differ diff --git a/ApiExample/entry/src/main/resources/base/media/foreground.png b/ApiExample/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000..4483dda Binary files /dev/null and b/ApiExample/entry/src/main/resources/base/media/foreground.png differ diff --git a/ApiExample/entry/src/main/resources/base/media/layered_image.json b/ApiExample/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000..fb49920 --- /dev/null +++ b/ApiExample/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/resources/base/media/scan.png b/ApiExample/entry/src/main/resources/base/media/scan.png new file mode 100644 index 0000000..ac4d934 Binary files /dev/null and b/ApiExample/entry/src/main/resources/base/media/scan.png differ diff --git a/ApiExample/entry/src/main/resources/base/media/startIcon.png b/ApiExample/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000..205ad8b Binary files /dev/null and b/ApiExample/entry/src/main/resources/base/media/startIcon.png differ diff --git a/ApiExample/entry/src/main/resources/base/profile/backup_config.json b/ApiExample/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000..d742c2f --- /dev/null +++ b/ApiExample/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/resources/base/profile/main_pages.json b/ApiExample/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/ApiExample/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/ApiExample/entry/src/main/resources/en_US/element/string.json b/ApiExample/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..d85dd00 --- /dev/null +++ b/ApiExample/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ApiExample" + } + ] +} \ No newline at end of file diff --git a/ApiExample/entry/src/main/resources/zh_CN/element/string.json b/ApiExample/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..2b4ed21 --- /dev/null +++ b/ApiExample/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ApiExample" + } + ] +} \ No newline at end of file diff --git a/ApiExample/entry/src/mock/mock-config.json5 b/ApiExample/entry/src/mock/mock-config.json5 new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/ApiExample/entry/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/ApiExample/entry/src/ohosTest/ets/test/Ability.test.ets b/ApiExample/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..85c78f6 --- /dev/null +++ b/ApiExample/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/ApiExample/entry/src/ohosTest/ets/test/List.test.ets b/ApiExample/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..794c7dc --- /dev/null +++ b/ApiExample/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/ApiExample/entry/src/ohosTest/module.json5 b/ApiExample/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000..ae666e1 --- /dev/null +++ b/ApiExample/entry/src/ohosTest/module.json5 @@ -0,0 +1,13 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/ApiExample/entry/src/test/List.test.ets b/ApiExample/entry/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/ApiExample/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/ApiExample/entry/src/test/LocalUnit.test.ets b/ApiExample/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/ApiExample/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/ApiExample/hvigor/hvigor-config.json5 b/ApiExample/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..e0bd0d8 --- /dev/null +++ b/ApiExample/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.0", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/ApiExample/hvigorfile.ts b/ApiExample/hvigorfile.ts new file mode 100644 index 0000000..f3cb9f1 --- /dev/null +++ b/ApiExample/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/ApiExample/oh-package-lock.json5 b/ApiExample/oh-package-lock.json5 new file mode 100644 index 0000000..6302d7b --- /dev/null +++ b/ApiExample/oh-package-lock.json5 @@ -0,0 +1,27 @@ +{ + "meta": { + "stableOrder": true + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.19": "@ohos/hypium@1.0.19" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.19": { + "name": "@ohos/hypium", + "version": "1.0.19", + "integrity": "sha512-cEjDgLFCm3cWZDeRXk7agBUkPqjWxUo6AQeiu0gEkb3J8ESqlduQLSIXeo3cCsm8U/asL7iKjF85ZyOuufAGSQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.19.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/ApiExample/oh-package.json5 b/ApiExample/oh-package.json5 new file mode 100644 index 0000000..dd6b0d4 --- /dev/null +++ b/ApiExample/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "5.0.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.19", + "@ohos/hamock": "1.0.0" + } +} diff --git a/README.md b/README.md index c57f1fc..98d14bb 100644 --- a/README.md +++ b/README.md @@ -1 +1,141 @@ -"# QNRTC-HarmonyOS" +# 1 概述 + +QNRTCKit 是七牛推出的一款适用于 HarmonyOS 平台的音视频通话 SDK,提供了包括音视频通话、单路推流等多种功能,提供灵活的接口,支持高度定制以及二次开发。 + +# 2 功能列表 + +- 支持 pc 重连超时、mcu 获取超时时间 +- 支持 mcu 备用域名设置 +- 支持硬件编码 +- 支持重连超时配置 +- 支持视频降级默认值区分场景 +- 支持本地和远端视频 stats 回调宽高信息 +- 支持切换摄像头结果回调 +- 支持设置视频在弱网下的降级模式 +- 支持日志文件上传 +- 支持动态修改编码配置 +- 支持推流固定分辨率 +- 支持获取本地或远端音频数据音量等级 +- 支持视频 SEI +- 支持单路转推 +- 支持使用 TCP 传输媒体流 +- 支持音视频分别发布 +- 支持纯音频连麦 +- 支持码率上下限配置 +- 支持多分辨率编码 +- 支持静音功能 +- 支持实时状态回调 +- 支持回调连麦房间统计信息 +- 支持丰富的连麦消息回调 + +# 3 方案介绍 +七牛实时音视频云支持低延时音视频通话,提供灵活丰富的接口,方便进行二次开发。该系统主要包括服务端和客户端两个部分,其中,服务端主要提供了房间管理、权限验证、信令和媒体数据转发等功能,客户端则提供了媒体数据的采集、编解码、传输、渲染等功能。 + +### 3.1 系统框图 +![](http://docs.qnsdk.com/qnrtc-overview-architecture.png) + +整个系统的架构如上图所示,主要分为三个部分: + +- 客户端 SDK + + 主要负责客户端的音视频采集、渲染、滤镜处理、编解码、传输等工作,客户可以快速集成到自己 App 中,让自己的应用具备音视频通话的能力 + +- 服务端 REST API 和 SDK + + 主要提供房间管理、状态回调等基本的业务功能,另外还提供鉴黄鉴暴、质量分析等配套功能 + +- 服务器 + + 主要负责信令交互、音视频传输、代理加速等工作,保证音视频互动延时低,可用性高 + +### 3.2 交互流程 + +![](http://docs.qnsdk.com/qnrtc-interactive.png) + +实时通话交互流程如上图所示,因此,App 服务端需要开发的工作如下: + +- 为用户创建通话房间,并将通话房间和对应主播的 Id 关联起来 +- 计算加入房间的 roomToken 并提供给 App,该 roomToken 是结合 userId、roomName 等信息使用七牛的 AccessKey 和 SecretKey 按照一定的规则生成 +- 提供通话的业务逻辑,如:通话请求/应答业务逻辑、服务端房间管理和踢人等 + +关于 roomToken 的计算方法请查阅[《七牛实时音视频云服务端 API 接口规范》](https://developer.qiniu.com/rtc/8805/server-overview),另外,我们也提供了多种开发语言的 SDK [服务端开发手册及 SDK 下载](https://developer.qiniu.com/rtc/8812/serversdk)。 + +### 3.3 房间管理 +关于音视频通话房间的 API 主要分为两个部分,一部分在客户端,另一部分在服务端。在客户端 SDK 中,只有加入/离开连麦房间的接口。我们把创建/销毁连麦房间的功能放到了服务端,由 App Server 向七牛的服务器发送请求来完成。关于服务端 API 的详细内容,请查阅[《七牛实时音视频云服务端 API 接口规范》](https://developer.qiniu.com/rtc/8805/server-overview)。 + +# 4 方案优势 +- 实时互动对网络的稳定性和连通性要求非常苛刻,所以必须购买数据中心建设基础网络。而使用七牛的实时音视频云服务,不需要投入大量资金做传输网络的基础建设,按量计费灵活方便。 +- 经验丰富的音视频团队提供稳定、易用的客户端 SDK,保证了客户端应用开发的效率和可用性。 +- 完整的音视频产品线,使用七牛的实时音视频云服务的同时可以无缝接入七牛其他的所有服务,例如短视频、直播、存储、大数据分析等服务。 + +# 5 应用场景 + +## 5.1 主播连麦 + +- 支持主播之间连麦一起直播,带来与传统单向直播不一样的体验 +- 自带美颜滤镜,第三方美颜贴纸、人脸识别,让直播过程更有趣 +- 48KHz 采样率、全频带编解码以及对音乐场景的特殊优化保证观众可以听到最优质的声音 + +## 5.2 视频会议 + +- 小范围实时音视频互动,提供多种视频通话布局模板,更提供自定义布局方式,保证会议发言者互相之间的实时性,提升普通观众的观看体验 +- 提供七牛云自有的直播分发服务,可实现 HLS、RTMP、HTTP 等多种直播分发形式,支持更多人通过拉取直播流收看会议内容,适合大型的企业在线会议 +- 支持动态邀人,踢人、禁音,禁视会议权限分级控制 +- 客户可以利用七牛实时音视频云轻松做出一款类似 WebEx 的应用 + +## 5.3 一对一社交 + +- 客户可以利用七牛实时音视频云实现 QQ、微信、陌陌等社交应用的一对一视频互动 +- 提供七牛云自有的直播分发服务,可实现 HLS、RTMP、HTTP 等多种直播分发形式,画面清晰、声音清晰不卡顿 +- 自带美颜滤镜,第三方美颜贴纸、人脸识别,让社交更加有趣 + +## 5.4 狼人杀游戏 + +- 支持 12 人视频通话,玩家可在游戏中选择只开启语音或同时开启音视频 +- 提供七牛云自有的直播分发服务,可实现 HLS、RTMP、HTTP 等多种直播分发形式,音视频体验稳定流畅,不卡麦,不黑麦 +- 提供美颜和贴纸等功能,不断增强用户粘性,提升用户的活跃度和 DAU + +## 5.5 在线教育 + +- 自定义的视频布局功能允许开发者按照自己的业务需求调整老师和学生的显示位置 +- 旁路直播功能加上直播云的直播功能,实现观众人数无上限,让更多学生享受在线教育的便利 +- 搭配使用聊天室功能,文字、语音、图片、视频包括自定义消息等,更多的互动方式有效提升课堂氛围 +- 服务端录制对接点播平台,支持课程录制以及在线回放,让优质资源服务更多学生 + +## 5.6 在线抓娃娃 + +- 娃娃机端,通过主板或 PC 机连接两个摄像头,采集视频数据 +- 通过编码器编码,进行视频流的优化,通过实时流网络进行视频实时传输,最后到达操作端,解码、播放 +- 操作端通过业务 Server 将操控指令发送给娃娃机端,通过视频流获得实时反馈 +- 采用 WebSocket 技术,结合成熟稳定的直播云端,突破了 HLS 高延迟的技术限制,同时还能保持 H5 的传播便捷特性 + +## 5.7 在线客服 + +- 线上开展音视频对话,对客户的资信情况进行审核,方便金融科技企业实现用户在线签约、视频开户验证以及呼叫中心等功能 +- 提供云端存储空间及海量数据的处理能力,提供高可用的技术和高稳定的平台 + +# 6 开发文档 + +- 可通过 [QNRTCKit 快速入门](https://developer.qiniu.com/rtc/12831/quick_start-HarmonyOS) 了解如何快速搭建音视频通话应用 +- 可通过 [QNRTCKit 使用指南](https://developer.qiniu.com/rtc/12837/development_guidelines-HarmonyOS) 了解不同场景的实现方式 +- 可通过 [QNRTCKit API 概览](https://developer.qiniu.com/rtc/12830/ApiGuide-HarmonyOS) 了解 SDK 的接口设计及使用姿势 + +# 7 反馈及意见 + +当你遇到任何问题时,可以通过在 GitHub 的 repo 提交 issues 来反馈问题,请尽可能的描述清楚遇到的问题,如果有错误信息也一同附带,并且在 Labels 中指明类型为 bug 或者其他。 + +[通过这里查看已有的 issues 和提交 Bug](https://github.com/pili-engineering/QNRTC-HarmonyOS/issues) + +# 8 FAQ + +## 8.1 实时通话功能是否收费? + +客户端 SDK 不收费,服务端可按照带宽、流量或者时长收费,具体请联系七牛商务或者技术支持。 + +## 8.2 实时通话对讲延时多大? + +正常网络条件下,对讲延时在 200-300ms 左右。 + +## 8.3 是否有服务端的 SDK 或者 demo 代码可以参考? + +有的,请参考: [QNRTC-Server](https://developer.qiniu.com/rtc/8812/serversdk) diff --git a/ReleaseNotes/release-notes-1.0.0.md b/ReleaseNotes/release-notes-1.0.0.md new file mode 100644 index 0000000..1a9d439 --- /dev/null +++ b/ReleaseNotes/release-notes-1.0.0.md @@ -0,0 +1,44 @@ +# QNRTCKit Release Notes for 1.0.0 + +## 内容 + +- [简介](#简介) +- [功能](#功能) +- [问题反馈](#问题反馈) + + +## 简介 + +QNRTCKit 是七牛推出的一款适用于 HarmonyOS 平台的音视频通话 SDK,提供了包括音视频通话、单路推流等多种功能,提供灵活的接口,支持高度定制以及二次开发。 + + +## 功能 +- 支持 pc 重连超时、mcu 获取超时时间 +- 支持 mcu 备用域名设置 +- 支持硬件编码 +- 支持重连超时配置 +- 支持视频降级默认值区分场景 +- 支持本地和远端视频 stats 回调宽高信息 +- 支持切换摄像头结果回调 +- 支持设置视频在弱网下的降级模式 +- 支持日志文件上传 +- 支持动态修改编码配置 +- 支持推流固定分辨率 +- 支持获取本地或远端音频数据音量等级 +- 支持视频 SEI +- 支持单路转推 +- 支持使用 TCP 传输媒体流 +- 支持音视频分别发布 +- 支持纯音频连麦 +- 支持码率上下限配置 +- 支持多分辨率编码 +- 支持静音功能 +- 支持实时状态回调 +- 支持回调连麦房间统计信息 +- 支持丰富的连麦消息回调 + +## 问题反馈 + +当你遇到任何问题时,可以通过在 GitHub 的 repo 提交 ```issues``` 来反馈问题,请尽可能的描述清楚遇到的问题,如果有错误信息也一同附带,并且在 ```Labels``` 中指明类型为 bug 或者其他。 + +[通过这里查看已有的 issues 和提交 bug](https://github.com/pili-engineering/QNRTC-HarmonyOS/issues) \ No newline at end of file diff --git a/releases/qnrtckit-1.0.0.zip b/releases/qnrtckit-1.0.0.zip new file mode 100644 index 0000000..22cb56a Binary files /dev/null and b/releases/qnrtckit-1.0.0.zip differ