HarmonyOS 鸿蒙Next中XComponent的视频会议
HarmonyOS 鸿蒙Next中XComponent的视频会议
XComponent的视频会议上面有三个用户下面一个大屏,点击摄像头则播放自己的点击其他用户则播放其他用户的视频大屏,但是我现在是点击其他用户无法进行切换需要先点击摄像头关闭自己的然后点击其他用户列表点击摄像头才可以播放的问题怎样才可以解决,还有开启或者关闭刷新中遇到的空指针问题一般怎么处理
开发者您好,请参考停止XComponent渲染数据,尝试通过在XComponent组件外部添加if判断,实现XComponent组件的创建,销毁。如仍然不能解决问题,请说明具体原因并提供日志信息。针对第二个空指针闪退问题,请提供CPPCrash日志和hilog日志,以便分析定位。
更多关于HarmonyOS 鸿蒙Next中XComponent的视频会议的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
开发者您好,针对第一个会议窗口切换问题,您可以参考线上会议主副窗口切换对本地代码进行修改,本示例基于WindowStage、XComponent和CameraManager实现开启视频和切换主副窗口内容的功能。如果依然不能解决问题,请您提供能复现问题的demo。
对第二个刷新空指针问题,请您提供以下信息:
- 具体的使用场景以及如何复现问题。
[@ComponentV2](/user/ComponentV2)
export struct HomePage {
private DOMAIN = 0x00
private TAG = "HomePage"
[@Local](/user/Local) containerWidth: number = 0
[@Local](/user/Local) containerHeight: number = 0
[@Require](/user/Require) [@Param](/user/Param) windowIsLandscape: boolean = false
[@Require](/user/Require) [@Param](/user/Param) userContext: VVUserContext
[@Require](/user/Require) [@Param](/user/Param) bigScreenUserId: number
[@Event](/user/Event) itemSingleClick: (rtmUid: number) => void = () => {
}
[@Event](/user/Event) itemDoubleClick: (rtmUid: number, xcompentId: string) => void = () => {
}
[@Event](/user/Event) areaClick: () => void = () => {
}
aboutToAppear(): void {
}
aboutToDisappear(): void {
}
build() {
RelativeContainer() {
if (this.userContext.getUsersCount() == 1) {
Row() {
MemberView({ user: this.userContext.getUserList()[0], xComponentIdPrefixx: "HomePage-Only-One", isSmallScreen: false, itemDoubleClick: this.itemDoubleClick })
.width("100%")
.height("100%")
.onClick(() => {
this.areaClick()
})
}.onVisibleAreaChange([0, 0.2], (isExpanding: boolean, currentRatio: number) => {
if (isExpanding && currentRatio >= 0.2) {
//可见
VisibleUser.appendHomePageVisibleUser(this.userContext.getUserList().length > 0 ? this.userContext.getUserList()[0].userId : 0)
} else if (!isExpanding && currentRatio <= 0) {
//不可见
VisibleUser.removeHomePageVisibleUser(this.userContext.getUserList().length > 0 ? this.userContext.getUserList()[0].userId : 0)
}
})
} else if (this.userContext.getUsersCount() == 2) {
Row() {
MemberView({ user: this.userContext.getUserList()[0], xComponentIdPrefixx: `HomePage-One${this.userContext.getUserList()[0].reloadIndex}`, isSmallScreen: true, itemDoubleClick: this.itemDoubleClick })
.width(this.containerWidth / 2 - 1).aspectRatio(this.windowIsLandscape ? 16 / 9 : 9 / 16)
.gesture(GestureGroup(
GestureMode.Exclusive,
TapGesture({ count: 1 }).onAction(() => {
this.areaClick()
})
))
Row().layoutWeight(1)
MemberView({ user: this.userContext.getUserList()[1], xComponentIdPrefixx: `HomePage-Two${this.userContext.getUserList()[0].reloadIndex}`, isSmallScreen: true, itemDoubleClick: this.itemDoubleClick})
.width(this.containerWidth / 2 - 1).aspectRatio(this.windowIsLandscape ? 16 / 9 : 9 / 16)
.gesture(GestureGroup(
GestureMode.Exclusive,
TapGesture({ count: 1 }).onAction(() => {
this.areaClick()
})
))
}.width("100%")
.height("auto")
.alignRules({
center: {
anchor: "__container__",
align: VerticalAlign.Center
}
}).onVisibleAreaChange([0, 0.2], (isExpanding: boolean, currentRatio: number) => {
if (isExpanding && currentRatio >= 0.2) {
//可见
VisibleUser.appendHomePageVisibleUsers(this.userContext.getUserList().map((value, index, array) => {
return value.userId
}))
} else if (!isExpanding && currentRatio <= 0) {
//不可见
VisibleUser.removeHomePageVisibleUsers(this.userContext.getUserList().map((value, index, array) => {
return value.userId
}))
}
})
} else if (this.userContext.getUsersCount() >= 3) {
List({ space: 5 }) {
Repeat<User>(this.userContext.getUserList()).each(() => {
}).template("", (item: RepeatItem<User>) => {
ListItem() {
MemberView({
user: item.item,
xComponentIdPrefixx: "HomePage-List",
isSmallScreen: true,
showVideoPlaceholder: this.bigScreenUserId === item.item.userId,
}).aspectRatio(this.windowIsLandscape ? 4 / 3 : 3 / 4)
.width(this.windowIsLandscape ? "100%" : undefined)
.height(this.windowIsLandscape ? undefined : "100%")
.border({
width: item.item.rtmUid == this.bigScreenUserId ? 1 : 0,
color: $r("app.color.00D53B")
})
}
// .visibility(item.item.rtmUid === this.bigScreenUserId ? Visibility.None : Visibility.Visible)
.onClick(() => {
// 问题应该出现在这里
this.itemSingleClick(item.item.rtmUid)
})
.onVisibleAreaChange([0, 0.2], (isExpanding: boolean, currentRatio: number) => {
if (isExpanding && currentRatio >= 0.2) {
//可见
VisibleUser.appendHomePageVisibleUser(item.item.userId)
} else if (!isExpanding && currentRatio <= 0) {
//不可见
VisibleUser.removeHomePageVisibleUser(item.item.userId)
}
})
}, { cachedCount: 0 }).virtualScroll({ totalCount: this.userContext.getUsersCount() })
}
.cachedCount(0)
.id("listMember")
.listDirection(this.windowIsLandscape ? Axis.Vertical : Axis.Horizontal)
.scrollBar(BarState.Off)
.width(this.windowIsLandscape ? this.containerWidth * 0.2 : "100%")
.height(this.windowIsLandscape ? "100%" : this.containerHeight * 0.2)
.margin(this.windowIsLandscape ? { top: 5, bottom: 5 } : { top: 60, left: 5, right: 5 })
.padding(this.windowIsLandscape ? { left: 5, right: 5 } : { top: 5, bottom: 5 })
.alignRules(this.windowIsLandscape ? {
right: {
anchor: "__container__",
align: HorizontalAlign.End
}
} : {
top: {
anchor: "__container__",
align: VerticalAlign.Top
}
})
Row() {
MemberView({
user: this.userContext.getPeerByRtmuid(this.bigScreenUserId),
xComponentIdPrefixx: "HomePage-Big",
isSmallScreen: false,
itemDoubleClick: this.itemDoubleClick
}).gesture(GestureGroup(
GestureMode.Exclusive,
TapGesture({ count: 1 }).onAction(() => {
this.areaClick()
})
))
}
.alignRules(this.windowIsLandscape ? {
left: {
anchor: "__container__",
align: HorizontalAlign.Start
},
right: {
anchor: "listMember",
align: HorizontalAlign.Start
},
top: {
anchor: "__container__",
align: VerticalAlign.Top
},
bottom: {
anchor: "__container__",
align: VerticalAlign.Bottom
}
} : {
left: {
anchor: "__container__",
align: HorizontalAlign.Start
},
right: {
anchor: "__container__",
align: HorizontalAlign.End
},
top: {
anchor: "listMember",
align: VerticalAlign.Bottom
},
bottom: {
anchor: "__container__",
align: VerticalAlign.Bottom
}
})
.onVisibleAreaChange([0, 0.2], (isExpanding: boolean, currentRatio: number) => {
if (isExpanding && currentRatio >= 0.2) {
//可见
VisibleUser.appendHomePageVisibleUser(this.bigScreenUserId)
} else if (!isExpanding && currentRatio <= 0) {
//不可见
VisibleUser.removeHomePageVisibleUser(this.bigScreenUserId)
}
})
}
}.backgroundColor($r("app.color.black"))
.onAreaChange((oldValue, newValue) => {
this.containerWidth = newValue.width as number
this.containerHeight = newValue.height as number
// this.windowIsLandscape = this.containerWidth > this.containerHeight
})
}
}
[@ComponentV2](/user/ComponentV2)
export struct MemberView {
private TAG = "MemberView"
[@Event](/user/Event) itemDoubleClick: (userId: number, xcompentId: string) => void = () => {
}
[@Require](/user/Require) [@Param](/user/Param) user: User
[@Require](/user/Require) [@Param](/user/Param) xComponentIdPrefixx: string
//由于声网多控件渲染有问题, 所以设置当需要渲染视频时, 当前控件时渲染视频, 还是只是放一个占位图表示当前为视频
[@Param](/user/Param) showVideoPlaceholder: boolean = false
[@Require](/user/Require) [@Param](/user/Param) isSmallScreen: boolean
//当前控件宽高的最小值
private rectWidth: number | undefined = undefined
//头像控件的宽度
[@Param](/user/Param) headerWidth: number | undefined = undefined
[@Local](/user/Local) headerWidthLocal: number = 0
//麦克风图标控件的宽度
[@Param](/user/Param) micWidth: number | undefined = undefined
[@Local](/user/Local) micWidthLocal: number = 0
//margin的宽度
[@Param](/user/Param) marginWidth: number | undefined = undefined
[@Local](/user/Local) marginWidthLocal: number = 0
//麦克风图标与姓名之间的宽度
[@Param](/user/Param) micNameMargin: number | undefined = undefined
[@Local](/user/Local) micNameMarginLocal: number = 0
//姓名控件的文字大小
[@Param](/user/Param) textSize: number | undefined = undefined
[@Local](/user/Local) textSizeLocal: number = 0
// 王老师的会导致id重复
// getXComponentId(): string {
// let id = this.user.membervideoXComponentId
// return `${this.xComponentIdPrefixx}-${id}`
// }
//我的添加随机数禁止id重复引发系统崩溃
getXComponentId(): string {
let id = this.user?.membervideoXComponentId
if (!id) {
id = `${Date.now()}${Math.random().toString(36).substr(2, 9)}`
}
return `${this.xComponentIdPrefixx}-${id}`
}
aboutToAppear(): void {
// hilog.error(this.DOMAIN, this.TAG, "aboutToAppear: uid = %{public}d", this.user?.userId)
}
aboutToDisappear(): void {
// hilog.error(this.DOMAIN, this.TAG, "aboutToDisappear: uid = %{public}d", this.user?.userId)
}
build() {
RelativeContainer() {
if (this.user?.shareScreen && this.user?.rtmUid !== VVLocalUserManager.sharedManager().rtmUid) {
if (this.showVideoPlaceholder) {
Image($r("app.media.meeting_icon_video_placeholder"))
.objectFit(ImageFit.Contain)
.width("30%").height("30%")
.alignRules({
center: {
anchor: "__container__",
align: VerticalAlign.Center
},
middle: {
anchor: "__container__",
align: HorizontalAlign.Center
}
})
}
else {
XComponent({
id: this.getXComponentId(),
type: XComponentType.SURFACE,
libraryname: Constants.AGORA_LIB_NAME,
})
.key(this.getXComponentId())
.width("100%").height("100%")
.onLoad(() => {
new LogUtils('').error(this.TAG, `XComponent.shareScreen.onLoad: id = ${this.getXComponentId()}`)
if (!this.user) {
return
}
console.warn(`getXComponentId---11---${this.getXComponentId()}`)
console.warn(`getXComponentId---12---${this.user?.shareScreen}`)
console.warn(`getXComponentId---13---${this.user?.rtmUid}`)
console.warn(`getXComponentId---14---${VVLocalUserManager.sharedManager().rtmUid}`)
let canvas: VideoCanvas = VVRtcKitManager.sharedManager().getVideoCanvasById(this.getXComponentId(), MeetingConfig.shareScreenUserSubId)
console.warn(`getXComponentId---15---${canvas}`)
if (this.user?.rtmUid !== VVLocalUserManager.sharedManager().rtmUid) {
canvas.uid = MeetingConfig.shareScreenUserSubId
VVRtcKitManager.sharedManager().vvSetupRemoteVideo(canvas)
} else {
canvas.uid = 0
canvas.mirrorMode = VVLocalUserManager.sharedManager().openMirrorImage ? VideoCanvas.VIDEO_MIRROR_MODE_ENABLED : VideoCanvas.VIDEO_MIRROR_MODE_DISABLED
VVRtcKitManager.sharedManager().vvSetupLocalVideo(canvas)
}
})
.onDestroy(() => {
new LogUtils('').error(this.TAG, `XComponent.shareScreen.onDestroy: id = ${this.getXComponentId()}`)
})
}
}else if (this.user?.video) {
if (this.showVideoPlaceholder) {
Image($r("app.media.meeting_icon_video_placeholder"))
.objectFit(ImageFit.Contain)
.width("30%").height("30%")
.alignRules({
center: {
anchor: "__container__",
align: VerticalAlign.Center
},
middle: {
anchor: "__container__",
align: HorizontalAlign.Center
}
})
} else {
XComponent({
id: this.getXComponentId(),
type: XComponentType.SURFACE,
libraryname: Constants.AGORA_LIB_NAME,
})
.key(this.getXComponentId())
.width("100%").height("100%")
.onLoad(() => {
new LogUtils('').error(this.TAG, `XComponent.onLoad: id = ${this.getXComponentId()}`)
if (!this.user) {
return
}
console.warn(`getXComponentId---21---${this.getXComponentId()}`)
console.warn(`getXComponentId---22---${this.user?.shareScreen}`)
console.warn(`getXComponentId---23---${this.user?.rtmUid}`)
console.warn(`getXComponentId---24---${VVLocalUserManager.sharedManager().rtmUid}`)
let canvas: VideoCanvas = VVRtcKitManager.sharedManager().getVideoCanvasById(this.getXComponentId(), this.user.rtcUid)
console.warn(`getXComponentId---25---${JSON.stringify(canvas)}`)
if (this.user?.userId !== VVLocalUserManager.sharedManager().rtmUid) {
VVRtcKitManager.instance.vvSetupRemoteVideo(canvas)
} else {
canvas.uid = 0
canvas.mirrorMode = VVLocalUserManager.sharedManager().openMirrorImage ? VideoCanvas.VIDEO_MIRROR_MODE_ENABLED : VideoCanvas.VIDEO_MIRROR_MODE_DISABLED
VVRtcKitManager.instance.vvSetupLocalVideo(canvas)
}
})
.onDestroy(() => {
console.warn(`getXComponentId---222---销毁`)
new LogUtils('').error(this.TAG, `XComponent.onDestroy: id = ${this.getXComponentId()}}`)
})
}
} else {
HeaderView({
avatar: this.user?.avatar,
phoneStatus: this.user?.isinCalling(),
name: this.user?.userName,
rectWidth: this.headerWidth !== undefined ? this.headerWidth : this.headerWidthLocal,
devType: this.user?.devType,
showDeviceImg: false
})
.id("headerView")
.alignRules({
center: {
anchor: "__container__",
align: VerticalAlign.Center
},
middle: {
anchor: "__container__",
align: HorizontalAlign.Center
}
})
}
this.nameAndAudio()
}
.gesture(GestureGroup(
GestureMode.Exclusive,
TapGesture({ count: 2 }).onAction(() => {
this.itemDoubleClick(this.user.userId, this.getXComponentId())
})
))
.backgroundColor($r("app.color.232530"))
.onAreaChange((oldValue, newValue) => {
let rootWidth = newValue.width as number
let rootHeight = newValue.height as number
this.rectWidth = Math.min(rootWidth, rootHeight)
if (this.headerWidth === undefined) {
this.headerWidthLocal = Math.min(120, this.rectWidth * 0.45)
}
if (this.marginWidth === undefined) {
this.marginWidthLocal = Math.min(10, this.rectWidth * 0.03)
}
if (this.micNameMargin === undefined) {
this.micNameMarginLocal = Math.min(5, this.rectWidth * 0.03)
}
if (this.micWidth === undefined) {
this.micWidthLocal = Math.min(16, this.rectWidth * 0.1)
}
if (this.textSize === undefined) {
this.textSizeLocal = Math.min(16, this.rectWidth * 0.1)
}
})
}
[@Builder](/user/Builder)
nameAndAudio() {
if (this.user?.video || this.user?.shareScreen || this.isSmallScreen) {
Row() {
MicView({
viewHeight: this.micWidth !== undefined ? this.micWidth : this.micWidthLocal,
progress: this.user?.volume,
audio: this.user?.audio
}).在HarmonyOS Next中,XComponent组件支持视频会议功能,通过其提供的Surface能力实现视频流的渲染。开发者可使用ArkTS调用XComponent的接口,结合媒体服务进行视频数据的采集、编码与解码。XComponent能够创建EGL窗口,用于高效处理视频帧的绘制,确保低延迟和高性能。
在HarmonyOS Next中,使用XComponent实现视频会议时遇到切换逻辑和空指针问题,可以按以下思路解决:
1. 视频切换逻辑优化 核心在于管理好XComponent实例与视频流的绑定关系。建议采用状态集中管理,而非依赖UI操作顺序。例如:
- 为每个用户(包括本地)维护一个独立的
XComponentController。 - 点击用户时,直接调用对应控制器的
setVideoSource()方法,无需先关闭当前视频。 - 大屏显示通过一个独立的
XComponent实例控制,切换时只需更新其数据源。
关键代码逻辑参考:
// 统一管理视频源切换
switchVideoDisplay(targetUserId: string) {
// 1. 更新大屏XComponent的数据源为目标用户流
this.mainScreenController.setVideoSource(targetUserStream);
// 2. 可选:更新UI状态(如高亮当前选中用户)
this.updateUserSelection(targetUserId);
}
2. 空指针问题处理 在开启/关闭视频的异步操作中,空指针通常源于:
- XComponent生命周期回调(如
onSurfaceCreated)触发时,视频资源尚未准备就绪或已释放。 - 异步操作未做状态校验。
解决方案:
- 在
XComponentController的关键方法中添加空值守卫:
playVideo() {
if (!this.xComponentContext || !this.videoSource) {
// 记录日志或等待资源初始化
return;
}
// 执行播放逻辑
}
- 使用标志位管理组件状态(如
isSurfaceReady),在回调中更新状态,操作前校验。 - 对于异步关闭操作,建议先解绑监听器,再释放资源,避免回调触发时访问已释放对象。
3. 建议的架构优化
- 将视频流管理(获取、切换、释放)与UI操作解耦,通过状态监听更新界面。
- 考虑使用
VideoPlayer或AVPlayer配合XComponent进行视频渲染,它们提供了更完善的异步状态管理。
通过集中管理视频流状态和增加异步操作的保护性校验,可以解决切换不流畅和空指针异常的问题。

