鸿蒙跨端视频播放器:多设备同步播放控制 原创
鸿蒙跨端视频播放器:多设备同步播放控制
本文将基于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的媒体播放和分布式能力构建一个多设备同步的视频播放器。通过将播放状态存储在分布式数据库中,实现了播放进度、播放状态和控制指令的跨设备同步,为用户提供了无缝的视频观看体验。
这种架构不仅适用于视频播放场景,也可以扩展到音乐播放、直播观看等需要媒体同步的应用场景。合理利用鸿蒙的分布式能力,可以大大增强多设备协同应用的实用性和用户体验。




















