41 直播页整体架构:LiveBroadCastPage

系列五:直播与画中画 · 第 1 篇

对应工程:products/default/src/main/ets/pages/LiveBroadCastPage.etsproducts/pc/src/main/ets/pages/LiveBroadCastPage.ets

引言

系列四我们走完了"列表 → 详情 → 支付"的静态购物链路,从本篇开始进入系列五「直播与画中画」,也是整个项目技术含金量最高的部分:视频播放(AVPlayer)、Surface 渲染(XComponent)与画中画(PiPWindow)三大能力在直播页交汇。LiveBroadCastPage 是这条链路的"总装车间"——它决定页面由哪些区块组成、如何随断点切换手机/PC 布局、如何进入与退出、以及资源如何释放。本项目把它同时放在了 products/defaultproducts/pc 两套产品源码中,是「一次开发多端部署」中"产品层差异化"的典型样本。

页面组成:视频、评论、商品与头部

LiveBroadCastPage 本身不实现具体业务,它通过组合特性模块 multishoppinglive 的组件搭出完整直播页:

  • LiveContent:视频渲染区,内部是 XComponent + AVPlayer(系列五第 2、3 篇)。
  • LiveComment:滚动评论流,叠加在视频区左下角(第 5 篇)。
  • LiveShopList:带货商品列表,横条或侧栏两种形态(第 6 篇)。
  • LiveHeader / LiveHeaderPC:顶部主播信息栏(第 7 篇)。
  • LiveUtil:底部评论输入 + 分享/点赞/购物袋入口(第 8 篇联动购物袋)。
四个区块在 LiveBroadCast(直播舞台容器)中用 Stack 叠加:

Stack() {
  LiveContent({ widthBp: this.widthBp })               // 底层:视频
    .width('100%').height('100%')
  Column() {                                            // 中层:评论 + 横向商品条
    LiveComment({ widthBp: this.widthBp })
    LiveShopList({ isSideListLayout: false, ... })
  }
  Row() {                                               // 顶层:侧栏商品列表(大屏)
    LiveShopList({ isSideListLayout: true, ... })
  }
  .visibility(/* 非小屏断点才显示 */)
}

Stack 是直播页的灵魂:视频永远在最底层,评论、商品、头部都是"悬浮"在画面之上的半透明层,这正是直播间 UI 的通用范式。

进入方式:Navigation 与 navDestination

直播页通过全局路由栈进入。Index.ets 中注册了导航容器与页面映射:

private static readonly liveNavigationId: string = 'navid';

@Builder
PageMap(name: string) {
  if (name === 'RootTabsPage') {
    RootTabsPage()
  } else if (name === 'ProductDetail') {
    ProductDetail()
  } else if (name === 'LiveBroadCastPage') {
    LiveBroadCastPage()
  }
}

build() {
  HdsNavigation(this.mainPageMap) { RootTabsPage() }
    .navDestination(this.PageMap)
    .mode(NavigationMode.Stack)
    .id(Index.liveNavigationId)
}

注意两个"同名异义"的 id:

  • Index 中的 liveNavigationId = 'navid'Navigation 容器自己的组件 id。
  • LiveBroadCastPageLIVE_NAVIGATION_ID = 'navid' 同样挂在 HdsNavDestination/NavDestination 上:.id(LiveBroadCastPage.LIVE_NAVIGATION_ID)

第 4 篇会看到,这个 navid 是 PiP 创建 navigationId 时的关键入参——画中画需要知道"从哪个导航页面离开、回到哪个导航页面",两个 id 保持一致是画中画能正确回栈的前提。

页面内部通过 @Consumer('mainPageMap') 拿到全局共享的路由栈:

@Consumer('mainPageMap') public mainPageMap: NavPathStack = new NavPathStack();

private onLiveClose(): void {
  this.mainPageMap.pop();
}

PC 与默认产品的差异组合

两套 LiveBroadCastPage 源码的差异集中在三点:

差异点products/defaultproducts/pc

侧栏模式(MD/LG/XL)头部一律 LiveHeader使用 LiveHeaderPC
侧栏宽度固定 '50%'按断点 '50%'/'37.5%'
onLiveClose进入画中画(startPip 后 pop)直接 pop

两版共同的 isSideMode() 决定走"左右分栏"还是"上下全屏":

private isSideMode(): boolean {
  const bp = this.breakpoint.widthBreakpoint;
  return bp === WidthBreakpoint.WIDTH_MD || bp === WidthBreakpoint.WIDTH_LG || bp === WidthBreakpoint.WIDTH_XL;
}

大屏时 Row 左右排布:左侧直播舞台(宽 62.5%),右侧 sidePanel(37.5%,购物袋/支付卡);小屏时 Column 上下排布,并用 bindSheet 弹出 620vp 的半屏面板:

.bindSheet($$this.isDialogOpen, this.panelContent(), {
  height: '620vp',
  preferType: SheetType.CENTER,
  dragBar: false,
  enableOutsideInteractive: false,
  showClose: false,
  backgroundColor: $r('app.color.surface_secondary')
})

getLiveRenderWidthBp() 还有一个细节:打开侧栏后,直播舞台实际渲染宽度缩窄,断点要"降档"(SM→SM、LG→MD),否则直播组件会按全屏断点排版而显得拥挤:

private getLiveRenderWidthBp(): WidthBreakpoint {
  if (this.isDialogOpen && this.isSideMode()) {
    return new WidthBreakpointType(WidthBreakpoint.WIDTH_SM, WidthBreakpoint.WIDTH_SM,
      WidthBreakpoint.WIDTH_MD, WidthBreakpoint.WIDTH_MD).getValue(this.breakpoint.widthBreakpoint);
  }
  return this.breakpoint.widthBreakpoint;
}

此外,页面用 deviceInfo.distributionOSApiVersion >= 60100 做运行时降级:高版本用 HdsNavDestination(UIDesignKit),低版本退回原生 NavDestination,保证同一套页面在不同系统版本上都能运行。

生命周期与资源释放

直播页的资源释放是最容易踩坑的地方,本项目做了两层防护:

  • 视频层LiveContent.aboutToDisappear 中调用 avPlayerUtil.release(),把 AVPlayer 的 error/stateChange 回调注销并 release() 掉。
  • 画中画层LiveBroadCastPage.aboutToDisappear 中调用 pipWindowUtil.stopPip(),主动结束 PiP 并清理控制器回调:

aboutToDisappear(): void {
  if (this.pipWindowUtil === undefined) {
    return;
  }
  this.pipWindowUtil.stopPip();
}

两层的释放顺序有讲究:PiP 还在显示时若先释放 AVPlayer,画中画窗口会立刻黑屏;所以 PiP 的 stopPip 必须走"先停 PiP、再释放播放器"的链路。第 4 篇的 stopPipChain Promise 链就是为此设计的。

注意事项与总结

  • navid 一致性Index 的 Navigation id、LiveBroadCastPage 的 NavDestination id、PiP 的 navigationId 三者必须一致,否则画中画回栈会失效。
  • Stack 叠加层级:视频永远在底层,评论/商品/头部为悬浮层;半透明遮罩用 live_header_scrimlive_input_scrim 这类带透明度的资源色。
  • 断点降档:侧栏打开后直播舞台变窄,必须用 getLiveRenderWidthBp() 降档断点,否则商品条、评论宽度会溢出。
  • 资源释放:页面 aboutToDisappear 释放 PiP,视频组件 aboutToDisappear 释放 AVPlayer,二者都判空防护(pipWindowUtil === undefined)。
至此,LiveBroadCastPage 作为"总装车间"已经讲清:它组合了视频、评论、商品、头部四大区块,用断点切换手机半屏/PC 侧栏两种布局,并通过 navid 与 PiP 深度绑定。下一篇进入视频渲染的底层:XComponent 与 Surface。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐