
鸿蒙跨端视频播放器:多设备同步播放控制 原创
鸿蒙跨端视频播放器:多设备同步播放控制
本文将基于HarmonyOS的媒体播放能力和分布式技术,实现一个支持多设备同步的全屏视频播放器,能够在不同设备间同步播放状态、进度和控制指令。
技术架构
媒体播放层:使用AVPlayer实现视频播放功能
控制逻辑层:管理播放状态和用户交互
分布式同步层:通过分布式数据管理实现多设备同步
UI展示层:全屏播放界面和控制面板
完整代码实现
播放状态模型定义
// model/VideoPlaybackState.ts
export class VideoPlaybackState {
videoUrl: string = ‘’; // 视频URL
currentPosition: number = 0; // 当前播放位置(ms)
isPlaying: boolean = false; // 是否正在播放
duration: number = 0; // 视频总时长(ms)
lastControlDevice: string = ‘’; // 最后控制的设备ID
updateTime: number = 0; // 最后更新时间戳
constructor(data?: Partial<VideoPlaybackState>) {
if (data) {
Object.assign(this, data);
if (!this.updateTime) {
this.updateTime = Date.now();
}
// 格式化时间显示
get formattedPosition(): string {
return this.formatTime(this.currentPosition);
get formattedDuration(): string {
return this.formatTime(this.duration);
private formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return {minutes}:{seconds.toString().padStart(2, '0')};
}
分布式播放同步服务
// service/VideoSyncService.ts
import distributedData from ‘@ohos.data.distributedData’;
import deviceInfo from ‘@ohos.deviceInfo’;
import { VideoPlaybackState } from ‘…/model/VideoPlaybackState’;
const STORE_ID = ‘video_sync_store’;
const PLAYBACK_KEY = ‘video_playback_state’;
export class VideoSyncService {
private kvManager: distributedData.KVManager;
private kvStore: distributedData.SingleKVStore;
private localDeviceId: string = deviceInfo.deviceId;
// 初始化分布式数据存储
async initialize() {
const config = {
bundleName: ‘com.example.videoplayer’,
userInfo: {
userId: ‘video_user’,
userType: distributedData.UserType.SAME_USER_ID
};
this.kvManager = distributedData.createKVManager(config);
const options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
};
this.kvStore = await this.kvManager.getKVStore(STORE_ID, options);
// 订阅数据变更
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
this.handleDataChange(data);
});
// 处理数据变更
private handleDataChange(data: distributedData.ChangeNotification) {
if (data.insertEntries.length > 0 && data.insertEntries[0].key === PLAYBACK_KEY) {
const newState = JSON.parse(data.insertEntries[0].value.value);
AppStorage.setOrCreate(‘playbackState’, new VideoPlaybackState(newState));
}
// 同步播放状态
async syncPlaybackState(state: VideoPlaybackState) {
state.lastControlDevice = this.localDeviceId;
state.updateTime = Date.now();
await this.kvStore.put(PLAYBACK_KEY, JSON.stringify(state));
// 获取当前设备ID
getLocalDeviceId(): string {
return this.localDeviceId;
}
视频播放器组件实现
// components/VideoPlayerComponent.ets
import media from ‘@ohos.multimedia.media’;
import { VideoPlaybackState } from ‘…/model/VideoPlaybackState’;
@Component
export struct VideoPlayerComponent {
private avPlayer: media.AVPlayer;
@Link playbackState: VideoPlaybackState;
@State showControls: boolean = true;
@State isBuffering: boolean = false;
private controlHideTimer: number = 0;
private positionUpdateTimer: number = 0;
private syncService: VideoSyncService;
aboutToAppear() {
this.initPlayer();
onPageHide() {
this.cleanup();
build() {
Stack() {
// 视频播放视图
Video({
controller: this.avPlayer
})
.width('100%')
.height('100%')
.onClick(() => {
this.toggleControls();
})
// 缓冲指示器
if (this.isBuffering) {
LoadingProgress()
.width(50)
.height(50)
// 控制面板
if (this.showControls) {
this.buildControlPanel()
// 控制设备提示
if (this.playbackState.lastControlDevice &&
this.playbackState.lastControlDevice !== this.syncService.getLocalDeviceId()) {
Text(控制设备: ${this.getDeviceName(this.playbackState.lastControlDevice)})
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#66000000')
.padding(4)
.borderRadius(4)
.margin(10)
.align(Alignment.TopStart)
}
.width('100%')
.height('100%')
.backgroundColor('#000000')
@Builder
buildControlPanel() {
Column() {
// 顶部控制栏
Row() {
Button(‘返回’)
.fontColor(‘#FFFFFF’)
.onClick(() => {
router.back();
})
.width(‘100%’)
.padding(12)
// 中间控制区域
Row() {
Button('')
.icon($r('app.media.ic_skip_previous'))
.onClick(() => {
this.skipBackward(15000);
})
Button('')
.icon(this.playbackState.isPlaying ? r('app.media.ic_pause') : r('app.media.ic_play'))
.margin({ left: 20, right: 20 })
.onClick(() => {
this.togglePlayback();
})
Button('')
.icon($r('app.media.ic_skip_next'))
.onClick(() => {
this.skipForward(30000);
})
.margin({ top: 40, bottom: 40 })
// 底部控制栏
Column() {
// 进度条
Slider({
value: this.playbackState.currentPosition,
min: 0,
max: this.playbackState.duration,
step: 1000,
style: SliderStyle.OutSet
})
.width('90%')
.onChange((value: number) => {
this.seekTo(value);
})
// 时间显示
Row() {
Text(this.playbackState.formattedPosition)
.fontSize(12)
.fontColor('#FFFFFF')
Text('/')
.fontSize(12)
.fontColor('#FFFFFF')
.margin({ left: 4, right: 4 })
Text(this.playbackState.formattedDuration)
.fontSize(12)
.fontColor('#FFFFFF')
.margin({ top: 8 })
.alignItems(HorizontalAlign.Center)
.margin({ bottom: 20 })
.width(‘100%’)
.height('100%')
.backgroundBlur(10)
.onClick(() => {}) // 阻止点击穿透
// 初始化播放器
private async initPlayer() {
try {
this.avPlayer = new media.AVPlayer();
// 设置数据源
await this.avPlayer.setSource(this.playbackState.videoUrl);
await this.avPlayer.prepare();
// 获取视频时长
this.playbackState.duration = await this.avPlayer.getDuration();
// 恢复播放状态
if (this.playbackState.currentPosition > 0) {
await this.avPlayer.seek(this.playbackState.currentPosition);
if (this.playbackState.isPlaying) {
await this.avPlayer.play();
// 设置事件监听
this.setupEventListeners();
// 开始更新播放位置
this.startPositionUpdates();
catch (err) {
console.error('播放器初始化失败:', err);
}
// 设置事件监听器
private setupEventListeners() {
this.avPlayer.on(‘play’, () => {
this.playbackState.isPlaying = true;
this.syncPlaybackState();
});
this.avPlayer.on('pause', () => {
this.playbackState.isPlaying = false;
this.syncPlaybackState();
});
this.avPlayer.on('bufferingUpdate', (state: string) => {
this.isBuffering = state === 'start';
});
this.avPlayer.on('error', (err: Error) => {
console.error('播放器错误:', err);
prompt.showToast({ message: '播放错误: ' + err.message, duration: 3000 });
});
// 开始更新播放位置
private startPositionUpdates() {
this.positionUpdateTimer = setInterval(async () => {
try {
const currentPos = await this.avPlayer.getCurrentTime();
this.playbackState.currentPosition = currentPos;
// 每5秒同步一次位置
if (Date.now() - this.playbackState.updateTime > 5000) {
this.syncPlaybackState();
} catch (err) {
console.error('获取播放位置失败:', err);
}, 1000);
// 切换播放/暂停
private async togglePlayback() {
try {
if (this.playbackState.isPlaying) {
await this.avPlayer.pause();
else {
await this.avPlayer.play();
this.syncPlaybackState();
catch (err) {
console.error('切换播放状态失败:', err);
}
// 跳转到指定位置
private async seekTo(position: number) {
try {
await this.avPlayer.seek(position);
this.playbackState.currentPosition = position;
this.syncPlaybackState();
catch (err) {
console.error('跳转失败:', err);
}
// 快进
private async skipForward(ms: number) {
const newPos = Math.min(
this.playbackState.currentPosition + ms,
this.playbackState.duration
);
await this.seekTo(newPos);
// 快退
private async skipBackward(ms: number) {
const newPos = Math.max(
this.playbackState.currentPosition - ms,
);
await this.seekTo(newPos);
// 显示/隐藏控制面板
private toggleControls() {
this.showControls = !this.showControls;
// 3秒后自动隐藏控制面板
if (this.controlHideTimer) {
clearTimeout(this.controlHideTimer);
if (this.showControls) {
this.controlHideTimer = setTimeout(() => {
this.showControls = false;
}, 3000);
}
// 同步播放状态
private async syncPlaybackState() {
await this.syncService.syncPlaybackState(this.playbackState);
// 获取设备名称
private getDeviceName(deviceId: string): string {
return deviceId === this.syncService.getLocalDeviceId() ? ‘本设备’ : ‘其他设备’;
// 清理资源
private cleanup() {
if (this.avPlayer) {
this.avPlayer.release();
if (this.controlHideTimer) {
clearTimeout(this.controlHideTimer);
if (this.positionUpdateTimer) {
clearInterval(this.positionUpdateTimer);
}
视频播放页面实现
// pages/VideoPlayPage.ets
import { VideoPlaybackState } from ‘…/model/VideoPlaybackState’;
import { VideoSyncService } from ‘…/service/VideoSyncService’;
import { VideoPlayerComponent } from ‘…/components/VideoPlayerComponent’;
@Entry
@Component
struct VideoPlayPage {
private syncService: VideoSyncService = new VideoSyncService();
@StorageLink(‘playbackState’) playbackState: VideoPlaybackState = new VideoPlaybackState();
@State videoUrl: string = ‘’;
async aboutToAppear() {
await this.syncService.initialize();
// 从路由参数获取视频URL
const params = router.getParams();
if (params?.videoUrl) {
this.videoUrl = params.videoUrl;
this.playbackState.videoUrl = this.videoUrl;
}
build() {
Column() {
if (this.videoUrl) {
VideoPlayerComponent({
playbackState: $playbackState,
syncService: this.syncService
})
else {
Text('无效的视频地址')
.fontSize(18)
.fontColor('#FF0000')
}
.width('100%')
.height('100%')
.backgroundColor('#000000')
}
视频列表页面实现
// pages/VideoListPage.ets
@Entry
@Component
struct VideoListPage {
@State videos: Array<{title: string, url: string, thumb: Resource}> = [
title: ‘示例视频1’,
url: 'https://example.com/videos/sample1.mp4',
thumb: $r('app.media.video_thumb1')
},
title: ‘示例视频2’,
url: 'https://example.com/videos/sample2.mp4',
thumb: $r('app.media.video_thumb2')
];
build() {
Column() {
Text(‘视频列表’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 20 })
List() {
ForEach(this.videos, (video) => {
ListItem() {
VideoListItem({
video: video,
onTap: () => this.openVideoPlayer(video.url)
})
})
.layoutWeight(1)
.width('100%')
.width(‘100%’)
.height('100%')
private openVideoPlayer(url: string) {
router.pushUrl({
url: 'pages/VideoPlayPage',
params: { videoUrl: url }
});
}
@Component
struct VideoListItem {
@Prop video: {title: string, url: string, thumb: Resource};
@Prop onTap: () => void;
build() {
Column() {
Image(this.video.thumb)
.width(‘100%’)
.height(200)
.objectFit(ImageFit.Cover)
Text(this.video.title)
.fontSize(16)
.margin({ top: 8, bottom: 12 })
.width(‘90%’)
.margin({ bottom: 16 })
.onClick(() => {
this.onTap();
})
}
实现原理详解
播放同步机制:
主设备控制播放状态并同步到分布式数据库
从设备接收状态更新并同步本地播放器
显示数据来源设备信息
播放控制功能:
播放/暂停、快进/快退、进度跳转
自动隐藏的控制面板
缓冲状态指示
状态恢复策略:
页面切换时保存播放状态
重新进入时恢复播放位置
网络中断后自动重连
扩展功能建议
播放列表同步:
// 同步播放列表
async syncPlaylist(playlist: string[]) {
await this.kvStore.put(‘video_playlist’, JSON.stringify(playlist));
播放速率调整:
// 调整播放速率
async setPlaybackRate(rate: number) {
await this.avPlayer.setSpeed(rate);
this.playbackState.playbackRate = rate;
this.syncPlaybackState();
多设备角色控制:
// 设置主从设备关系
async setMasterDevice(deviceId: string) {
await this.kvStore.put(‘master_device’, deviceId);
总结
本文展示了如何利用HarmonyOS的媒体播放和分布式能力构建一个多设备同步的视频播放器。通过将播放状态存储在分布式数据库中,实现了播放进度、播放状态和控制指令的跨设备同步,为用户提供了无缝的视频观看体验。
这种架构不仅适用于视频播放场景,也可以扩展到音乐播放、直播观看等需要媒体同步的应用场景。合理利用鸿蒙的分布式能力,可以大大增强多设备协同应用的实用性和用户体验。
