HarmonyOS 「校园二手交易商城」App应用实战 42 XComponent 与视频渲染
42 XComponent 与视频渲染
系列五:直播与画中画 · 第 2 篇
对应工程:
features/multishoppinglive/src/main/ets/components/LiveContent.ets
引言
上一篇我们把直播页拆成了"总装车间",这一篇钻进视频渲染的底层。HarmonyOS 的视频画面不在普通组件里画——AVPlayer 解码出的视频帧需要一个"Surface 画布"来承载,而这个画布在 ArkUI 里就是 XComponent。它是 ArkUI 与系统原生渲染能力之间的桥:一端是 UI 组件树里的节点,另一端是 surfaceId 指向的原生 Surface。本项目 LiveContent.ets 完整演示了「XComponent 声明 → onLoad 拿 surfaceId → 交给 AVPlayer」的标准链路,并且它的 XComponentController 还被画中画复用——这是直播页能无缝进入 PiP 的关键设计。
XComponent 组件能力
XComponent 支持三种 type:
SURFACE:承载原生 Surface,视频/相机/OpenGL 渲染都走它。TEXTURE:纹理类型,用于相机等需要二次加工的流。NODE:原生节点,性能最高但侵入性最强。
SURFACE 即可。LiveContent 的声明如下:XComponent({
type: XComponentType.SURFACE,
controller: this.pipWindowUtil?.getXComponentController()
})
.onLoad(() => {
if (this.pipWindowUtil === undefined) {
return;
}
this.surfaceId = this.pipWindowUtil.getXComponentController().getXComponentSurfaceId();
if (this.avPlayerUtil === undefined) {
return;
}
this.avPlayerUtil.createAvPlayer(this.surfaceId).catch((error: BusinessError) => {
Logger.error(`Failed to create AvPlayer. Code: ${error.code}, message: ${error.message}`);
});
})两个细节值得注意:
- controller 来自
PipWindowUtil。XComponentController被PipWindowUtil以private mXComponentController = new XComponentController()持有并对外暴露getXComponentController()。这样同一个控制器实例既服务页面上的 XComponent,又会在画中画创建时作为componentController传入——PiP 窗口接管的是同一个 Surface,视频画面才能无缝"飞出"页面。 - onLoad 才拿 surfaceId。XComponent 的 Surface 是异步创建的,
getXComponentSurfaceId()必须在onLoad(Surface 就绪)之后调用,否则拿到空字符串,AVPlayer 绑定会失败。
surfaceId 的获取与传递
getXComponentSurfaceId() 返回的是 Surface 的 id 字符串。拿到后立即传给 avPlayerUtil.createAvPlayer(surfaceId),后续由 AVPlayer 封装类完成"绑定 surfaceId → prepare → play"的完整流程(第 3 篇详解)。这层封装的价值在于:UI 层只关心"我拿到了画布 id",播放器状态机与资源管理全部下沉到 AvPlayerUtil,组件代码保持极薄。
LiveContent 的完整字段很简单:
@ComponentV2
export struct LiveContent {
private static readonly VIDEO_ASPECT_RATIO: number = 16 / 9;
@Param public widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM;
@Local private surfaceId: string = '';
private pipWindowUtil: PipWindowUtil | undefined = PipWindowUtil.getPipWindowUtil(this.getUIContext());
private avPlayerUtil: AvPlayerUtil | undefined = AvPlayerUtil.getAvPlayerUtil(this.getUIContext());
}两个单例都在组件初始化时经 getUIContext() 获取——注意用的是 this.getUIContext() 而非全局 getContext(this),因为单例内部要通过 uiContext.getHostContext() 拿 resourceManager 读 rawfile(第 10 篇)。
多端尺寸适配
视频区最怕"变形"。LiveContent 的做法是:宽断点定宽度、窄断点定高度,再叠加固定 16:9 比例:
.width(new WidthBreakpointType('100%', '100%', 'auto', 'auto').getValue(this.widthBp))
.height(new WidthBreakpointType('auto', 'auto', '100%', '100%').getValue(this.widthBp))
.aspectRatio(LiveContent.VIDEO_ASPECT_RATIO)- XS/SM(手机竖屏):宽度撑满 100%,高度
auto,由 16:9 推导——视频横向铺满屏幕。 - MD/LG/XL(平板/PC 宽屏):高度撑满 100%,宽度
auto,由 16:9 推导——视频纵向铺满舞台。
Column().justifyContent(FlexAlign.Center) + 内层 Stack({ alignContent: Alignment.Center }),保证任何断点下视频都在舞台正中。外层 Stack 还挂了一个 onClick 做播放/暂停切换:Stack({ alignContent: Alignment.Center }) {
Column() { XComponent({ ... }) }
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.layoutWeight(1)
.onClick(() => {
if (this.avPlayerUtil === undefined) {
return;
}
this.avPlayerUtil.playerStateControl();
})playerStateControl 是"播放 ↔ 暂停"的总开关(第 9 篇展开),点击视频任意位置即可切换,符合直播间"点画面暂停"的交互直觉。
生命周期中的资源释放
视频组件消失时必须归还播放器,否则后台会残留解码线程与文件描述符:
aboutToDisappear(): void {
if (this.avPlayerUtil === undefined) {
return;
}
this.avPlayerUtil.release();
}注意这里只释放 AVPlayer,不碰 PiP——PiP 的停止由页面层 LiveBroadCastPage.aboutToDisappear 负责,职责分离避免双重释放。第 4 篇会看到 stopPip 内部还有 Promise 串行化防止并发调用冲突。
注意事项与总结
- type 别用错:视频播放必须
XComponentType.SURFACE;用 TEXTURE 会拿不到预期的 Surface 语义,NODE 类型则与 ArkUI 状态系统耦合更深。 - onLoad 是唯一安全时机:在
aboutToAppear里调getXComponentSurfaceId()大概率拿到空串,Surface 尚未创建。 - controller 全局唯一:XComponent 的 controller 要与 PiP 共用同一实例,这是"画面从页面平滑过渡到画中画"的前提;新建一个 controller 传给 PiP 会导致 PiP 窗口黑屏。
- 比例适配:用
aspectRatio(16/9)+ 断点控制"撑宽还是撑高",比硬编码宽高更抗多端变化。
AvPlayerUtil 的播放控制封装。
更多推荐


所有评论(0)