鸿蒙跨端视频播放器:多设备同步播放控制 原创

进修的泡芙
发布于 2025-6-18 22:07
浏览
0收藏

鸿蒙跨端视频播放器:多设备同步播放控制

本文将基于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的媒体播放和分布式能力构建一个多设备同步的视频播放器。通过将播放状态存储在分布式数据库中,实现了播放进度、播放状态和控制指令的跨设备同步,为用户提供了无缝的视频观看体验。

这种架构不仅适用于视频播放场景,也可以扩展到音乐播放、直播观看等需要媒体同步的应用场景。合理利用鸿蒙的分布式能力,可以大大增强多设备协同应用的实用性和用户体验。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐