HarmonyOS 鸿蒙Next续播
HarmonyOS 鸿蒙Next续播 不同组件间切换时,无法实现无缝续播如何解决
【背景知识】 AVPlayer集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。 AVPlayer需要从XComponent组件获取并设置surfaceId属性,用于设置显示画面。surfaceId属性支持在initialized状态下设置,支持在prepared/playing/paused/completed/stopped状态下重新设置,重新设置时确保已经在initialized状态下进行设置,否则重新设置失败,重新设置后视频播放在新的窗口渲染。
【解决方案】 将AVPlayer放进全局Map中,AVPlayer指定相应组件的surfaceId即可实现跨页面视频播放无缝转场。具体操作步骤如下:
- 在page1页面,通过GlobalContext将AVPlayer当做全局单例变量放到Map<string, media.AVPlayer>里面,通过router跳转到page2页面。
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { resourceManager } from '@kit.LocalizationKit';
export class GlobalContext {
private static instance: GlobalContext;
private _objects = new Map<string, media.AVPlayer>();
private constructor() {
}
public static getContext(): GlobalContext {
if (!GlobalContext.instance) {
GlobalContext.instance = new GlobalContext();
}
return GlobalContext.instance;
}
getObject(value: string): media.AVPlayer | undefined {
return this._objects.get(value);
}
setObject(key: string, objectClass: media.AVPlayer): void {
this._objects.set(key, objectClass);
}
}
@Entry
@Component
struct Page1 {
private surfaceID: string = '';
private avPlayer: media.AVPlayer | undefined = undefined;
private mXComponentController: XComponentController = new XComponentController();
async setMediaAsset(rawFilePath: string) {
if (this.avPlayer === undefined) {
console.error(`set media asset, avplayer is undefined`);
return;
}
let context = this.getUIContext().getHostContext()!;
let resMgr = context.resourceManager;
let rawFd: resourceManager.RawFileDescriptor | undefined = undefined;
try {
rawFd = resMgr.getRawFdSync(rawFilePath);
} catch (err) {
console.error(`get raw file description failed: ${err}`);
return;
}
let avFd: media.AVFileDescriptor = { fd: rawFd.fd, offset: rawFd.offset, length: rawFd.length };
this.avPlayer.fdSrc = avFd;
}
private setEventListening() {
this.avPlayer?.on('error', async (err: BusinessError) => {
console.error(`AVPlayer error: ${JSON.stringify(err)}`);
try {
await this.avPlayer?.reset();
} catch (error) {
console.error(`failed to invoke avplyer.reset: ${JSON.stringify(error)}`);
}
});
this.avPlayer?.on('stateChange', async (state: media.AVPlayerState) => {
console.info(`AVPlayer state change to ${state}`);
if (this.avPlayer === undefined) {
console.error('AVPlayer is undefined when state change');
return;
}
switch (state) {
case 'idle':
break;
case 'initialized':
this.avPlayer.surfaceId = this.surfaceID;
try {
await this.avPlayer.prepare();
} catch (error) {
console.info(`failed to invoke avplayer.prepare: ${JSON.stringify(error)}`);
}
break;
case 'prepared':
try {
await this.avPlayer.play();
} catch (error) {
console.info(`failed to invoke avplayer.play: ${JSON.stringify(error)}`);
}
break;
case 'playing':
break;
case 'completed':
break;
case 'paused':
break;
case 'stopped':
break;
case 'released':
break;
case 'error':
break;
default:
console.info(`AVPlayer change to unknown state: ${state}`);
break;
}
});
};
onJumpClick(): void {
this.getUIContext().getRouter().pushUrl({
url: 'pages/Page2',
}, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info(`Invoke pushUrl succeeded`);
});
}
build() {
Column({ space: 20 }) {
Text('Page1')
.fontSize(30)
.fontWeight(FontWeight.Bold);
XComponent({
type: XComponentType.SURFACE,
controller: this.mXComponentController,
})
.width('100%')
.aspectRatio(1)
.renderFit(RenderFit.RESIZE_CONTAIN)
.onLoad(async () => {
this.surfaceID = this.mXComponentController.getXComponentSurfaceId();
try {
this.avPlayer = await media.createAVPlayer();
GlobalContext.getContext().setObject('value', this.avPlayer);
this.setEventListening();
} catch (err) {
console.error(`failed to initialize avplayer`);
return;
}
await this.setMediaAsset('video.mp4');
});
Button('Jump Next')
.padding(5)
.fontSize(30)
.onClick(() => {
this.onJumpClick();
});
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
}
- 在page2页面,通过Map<string, media.AVPlayer>获取单例AVPlayer,将page2页面的XComponent的SurfaceId设置给AVPlayer。
import { media } from '@kit.MediaKit';
import { GlobalContext } from './Page1';
@Entry
@Component
struct Page2 {
private surfaceID: string = '';
private avPlayer: media.AVPlayer | undefined = undefined;
private mXComponentController: XComponentController = new XComponentController();
build() {
Column({ space: 20 }) {
Text('Page2')
.fontSize(30)
.fontWeight(FontWeight.Bold);
XComponent({
type: XComponentType.SURFACE,
controller: this.mXComponentController,
})
.width('100%')
.aspectRatio(1)
.renderFit(RenderFit.RESIZE_CONTAIN)
.onLoad(() => {
this.surfaceID = this.mXComponentController.getXComponentSurfaceId();
this.avPlayer = GlobalContext.getContext().getObject('value');
if (this.avPlayer) {
this.avPlayer.surfaceId = this.surfaceID;
}
});
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
}
更多关于HarmonyOS 鸿蒙Next续播的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
【背景知识】
Interface (AVPlayer):播放管理类,用于管理和播放媒体资源。在调用AVPlayer的方法前,需要先通过createAVPlayer()构建一个AVPlayer实例。
XComponent:提供用于图形绘制和媒体数据写入的Surface,XComponent负责将其嵌入到视图中,支持应用自定义Surface位置和大小。
【参考方案】
可参考视频列表页跳转到详情页无缝播放示例,通过AVPlayer与XComponent实现视频播放,通过切换AVPlayer的surfaceId控制不同XComponent播放视频实现转场效果,基于Window实现视频全屏播放。
-
自定义AVPlayer类,封装AVPlayer,使用XComponent播放视频。
XComponent({ type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(() => { this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); this.player.setSurfaceID(this.surfaceId); this.player.avPlayerFdSrc(this.getUIContext().getHostContext()!); }) -
自定义播放类中设置AVPlayer状态回调函数,在监听资源播放当前时间回调中放入新的surfaceId。
// 注册avplayer回调函数 setAVPlayerCallback(avPlayer: media.AVPlayer) { // 监听资源播放当前时间回调函数 avPlayer.on('timeUpdate', (time: number) => { this.currentTime = time; avPlayer.surfaceId = this.surfaceID; }); } -
XComponent加载时设置新的surfaceID,并通过触发AVPlayer状态回调更换播放组件。
XComponent() .onLoad(() => { this.surfaceId = this.xComponentController.getXComponentSurfaceId(); this.player.setSurfaceID(this.surfaceId); }) -
结合setWindowLayoutFullScreen与setPreferredOrientation方法实现视频全屏播放。
let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口 // 设置窗口全屏 let isLayoutFullScreen = true; windowClass.setWindowLayoutFullScreen(isLayoutFullScreen).then(() => { // ... }); // 设置窗口方向 export function setWindowDirection(orientation: number, ctx: Context) { window.getLastWindow(ctx).then((win) => { win.setPreferredOrientation(orientation).then((data) => { // ... }); }) }
HarmonyOS Next的续播功能基于分布式能力实现。应用通过AVSession接口管理媒体会话,系统会自动记录播放状态和进度。当用户切换设备时,鸿蒙的分布式数据管理会将播放信息同步到新设备,实现跨设备无缝续播。该功能需要应用适配AVSession框架,并在同一华为帐号下的设备间运行。
在HarmonyOS Next中,不同组件(如Video组件)间切换时实现无缝续播,核心在于状态管理与媒体会话的跨组件/页面生命周期管理。这并非组件自身功能,而是需要开发者通过ArkTS状态管理和媒体控制API来主动实现。
关键方案与步骤
-
全局状态管理:
- 将当前播放的媒体源(src)、播放位置(currentTime)、播放状态(playing/paused) 等关键信息,提升到全局状态(例如使用
AppStorage或LocalStorage)或通过UIAbility的Want传递。 - 这样,当从一个包含视频的组件/页面跳转到另一个时,新的组件能立即从全局状态中获取到之前的播放进度和状态。
- 将当前播放的媒体源(src)、播放位置(currentTime)、播放状态(playing/paused) 等关键信息,提升到全局状态(例如使用
-
媒体会话(AVSession)管理:
- 在第一个组件中创建
AVSession,并设置播放状态、元数据(如媒体标题)和播放控制命令(播放、暂停、跳转)。 - 在组件销毁(
aboutToDisappear)前,不要销毁AVSession,而是将其控制权移交或在全局状态中保持其关键信息(如sessionId)。 - 在第二个组件中,尝试获取或恢复之前的
AVSession(例如通过sessionId),或基于全局状态信息创建一个新的AVSession并恢复到指定进度。
- 在第一个组件中创建
-
具体实现流程:
- 组件A(播放中):
- 创建
AVSession,开始播放。 - 监听播放时间更新,并实时同步到全局状态(如
AppStorage)。 - 在跳转前,保存
AVSession的sessionId到全局状态。
- 创建
- 组件B(需续播):
- 在
aboutToAppear或初始化时,从全局状态获取sessionId和保存的播放位置。 - 尝试通过
avSession.getAVSessionById(sessionId)获取已有会话,或直接基于媒体源和保存的位置创建新会话。 - 调用
avSession.setAVPlaybackState()将播放状态设置为playing,并设置currentTime为保存的位置,然后开始播放。
- 在
- 组件A(播放中):
示例代码要点
// 全局状态定义(例如在AppStorage中)
AppStorage.setOrCreate<number>('lastPlayPosition', 0);
AppStorage.setOrCreate<string>('lastSessionId', '');
// 组件A:保存状态
async function savePlayState() {
let currentPos = videoController.currentTime; // 获取当前播放位置
AppStorage.set<number>('lastPlayPosition', currentPos);
AppStorage.set<string>('lastSessionId', avSession.sessionId);
}
// 组件B:恢复播放
async function restorePlayback() {
let lastPos = AppStorage.get<number>('lastPlayPosition');
let lastSessionId = AppStorage.get<string>('lastSessionId');
// 尝试获取已有会话或创建新会话
let session = await avSession.getAVSessionById(lastSessionId);
if (!session) {
// 创建新会话,并设置到指定位置
session = await avSession.createAVSession(...);
}
// 设置播放状态和位置
let playbackState: avSession.AVPlaybackState = {
state: avSession.PlaybackState.PLAYING,
currentTime: lastPos
};
session.setAVPlaybackState(playbackState);
videoController.start(); // 开始播放
}
注意事项
- 后台播放:若需切后台后仍能续播,需申请
ohos.permission.KEEP_BACKGROUND_RUNNING权限,并在AVSession中配置后台播放能力。 - 生命周期协调:确保组件生命周期与
AVSession的生命周期正确绑定,避免资源泄露。 - 状态同步延迟:播放位置需高频更新,建议使用节流(throttle)方式同步到全局状态,以平衡性能与精度。
通过以上方案,即可在HarmonyOS Next中实现跨组件的无缝续播体验。

