鸿蒙跨端音乐同步:多设备协同音乐播放器

本文将基于HarmonyOS 5的AVPlayer和分布式能力,实现一个支持多设备协同的音乐播放器,可以在不同设备间同步播放状态、播放列表和播放进度。

技术架构

  1. ​媒体播放层​​:使用@ohos.multimedia.media的AVPlayer实现本地播放
  2. ​数据同步层​​:通过分布式数据管理实现播放状态同步
  3. ​设备管理层​​:发现和连接同一账号下的其他设备
  4. ​UI交互层​​:响应式UI展示播放状态和控制界面

完整代码实现

1. 音乐数据模型定义

// model/MusicItem.ts
export class MusicItem {
  id: string = '';          // 音乐ID
  title: string = '';       // 音乐标题
  artist: string = '';      // 艺术家
  album: string = '';       // 专辑
  duration: number = 0;     // 时长(毫秒)
  coverUrl: string = '';    // 封面URL
  filePath: string = '';    // 本地文件路径
  isRemote: boolean = false;// 是否为远程文件

  constructor(data?: any) {
    if (data) {
      this.id = data.id || '';
      this.title = data.title || '';
      this.artist = data.artist || '';
      this.album = data.album || '';
      this.duration = data.duration || 0;
      this.coverUrl = data.coverUrl || '';
      this.filePath = data.filePath || '';
      this.isRemote = data.isRemote || false;
    }
  }
}

// 播放状态枚举
export enum PlaybackState {
  IDLE = 'idle',
  PLAYING = 'playing',
  PAUSED = 'paused',
  STOPPED = 'stopped'
}

2. 本地音乐播放服务

// service/LocalPlayerService.ts
import media from '@ohos.multimedia.media';
import { MusicItem, PlaybackState } from '../model/MusicItem';

export class LocalPlayerService {
  private avPlayer: media.AVPlayer;
  private currentMusic: MusicItem | null = null;
  @State currentState: PlaybackState = PlaybackState.IDLE;
  private progressInterval: number = 0;
  @State currentPosition: number = 0;

  // 初始化播放器
  async initPlayer() {
    this.avPlayer = await media.createAVPlayer();
    
    // 监听播放器状态变化
    this.avPlayer.on('stateChange', (state: string) => {
      switch (state) {
        case 'idle':
          this.currentState = PlaybackState.IDLE;
          break;
        case 'playing':
          this.currentState = PlaybackState.PLAYING;
          this.startProgressTimer();
          break;
        case 'paused':
          this.currentState = PlaybackState.PAUSED;
          this.stopProgressTimer();
          break;
        case 'completed':
          this.currentState = PlaybackState.STOPPED;
          this.stopProgressTimer();
          break;
      }
    });
    
    // 监听错误事件
    this.avPlayer.on('error', (err) => {
      console.error('播放器错误:', err);
    });
  }

  // 播放音乐
  async play(music: MusicItem) {
    if (this.currentMusic?.id !== music.id) {
      this.currentMusic = music;
      await this.avPlayer.reset();
      await this.avPlayer.url = music.filePath;
      await this.avPlayer.prepare();
    }
    await this.avPlayer.play();
  }

  // 暂停播放
  async pause() {
    if (this.currentState === PlaybackState.PLAYING) {
      await this.avPlayer.pause();
    }
  }

  // 继续播放
  async resume() {
    if (this.currentState === PlaybackState.PAUSED) {
      await this.avPlayer.play();
    }
  }

  // 停止播放
  async stop() {
    await this.avPlayer.stop();
    this.stopProgressTimer();
  }

  // 跳转到指定位置
  async seekTo(position: number) {
    if (this.avPlayer) {
      await this.avPlayer.seek(position);
      this.currentPosition = position;
    }
  }

  // 开始进度计时器
  private startProgressTimer() {
    this.stopProgressTimer();
    this.progressInterval = setInterval(async () => {
      if (this.avPlayer && this.currentState === PlaybackState.PLAYING) {
        this.currentPosition = await this.avPlayer.getCurrentTime();
      }
    }, 1000);
  }

  // 停止进度计时器
  private stopProgressTimer() {
    if (this.progressInterval) {
      clearInterval(this.progressInterval);
      this.progressInterval = 0;
    }
  }

  // 释放资源
  async release() {
    await this.stop();
    if (this.avPlayer) {
      this.avPlayer.release();
    }
  }
}

3. 分布式音乐同步服务

// service/DistributedMusicService.ts
import distributedData from '@ohos.data.distributedData';
import deviceInfo from '@ohos.deviceInfo';
import { MusicItem, PlaybackState } from '../model/MusicItem';

const STORE_ID = 'music_sync_store';
const MUSIC_KEY = 'current_music';
const STATE_KEY = 'playback_state';
const POSITION_KEY = 'playback_position';

export class DistributedMusicService {
  private kvManager: distributedData.KVManager;
  private kvStore: distributedData.SingleKVStore;
  private localDeviceId: string = deviceInfo.deviceId;

  // 初始化分布式数据存储
  async init() {
    const config = {
      bundleName: 'com.example.musicplayer',
      userInfo: {
        userId: 'music_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.forEach(entry => {
        switch (entry.key) {
          case MUSIC_KEY:
            AppStorage.setOrCreate('currentMusic', JSON.parse(entry.value.value));
            break;
          case STATE_KEY:
            AppStorage.setOrCreate('playbackState', entry.value.value);
            break;
          case POSITION_KEY:
            AppStorage.setOrCreate('playbackPosition', entry.value.value);
            break;
        }
      });
    }
  }

  // 同步当前播放音乐
  async syncCurrentMusic(music: MusicItem | null) {
    await this.kvStore.put(MUSIC_KEY, JSON.stringify(music));
  }

  // 同步播放状态
  async syncPlaybackState(state: PlaybackState) {
    await this.kvStore.put(STATE_KEY, state);
  }

  // 同步播放位置
  async syncPlaybackPosition(position: number) {
    await this.kvStore.put(POSITION_KEY, position);
  }

  // 获取当前设备ID
  getLocalDeviceId(): string {
    return this.localDeviceId;
  }
}

4. 音乐播放器页面实现

// pages/MusicPlayerPage.ets
import { LocalPlayerService } from '../service/LocalPlayerService';
import { DistributedMusicService } from '../service/DistributedMusicService';
import { PlaybackState } from '../model/MusicItem';

@Entry
@Component
struct MusicPlayerPage {
  private localPlayer: LocalPlayerService = new LocalPlayerService();
  private distService: DistributedMusicService = new DistributedMusicService();
  @StorageLink('currentMusic') currentMusic: MusicItem | null = null;
  @StorageLink('playbackState') playbackState: PlaybackState = PlaybackState.IDLE;
  @StorageLink('playbackPosition') playbackPosition: number = 0;
  @State isControllerVisible: boolean = true;
  private controllerTimeout: number = 0;

  async aboutToAppear() {
    await this.localPlayer.initPlayer();
    await this.distService.init();
    
    // 监听播放状态变化
    AppStorage.link('playbackState', this, 'playbackState');
    AppStorage.link('playbackPosition', this, 'playbackPosition');
    
    // 设置控制器自动隐藏
    this.resetControllerTimer();
  }

  build() {
    Column() {
      // 音乐封面
      if (this.currentMusic) {
        Image(this.currentMusic.coverUrl || $r('app.media.default_cover'))
          .width(300)
          .height(300)
          .margin(20)
          .borderRadius(10)
          .onClick(() => {
            this.isControllerVisible = !this.isControllerVisible;
            this.resetControllerTimer();
          })
      } else {
        Image($r('app.media.default_cover'))
          .width(300)
          .height(300)
          .margin(20)
          .borderRadius(10)
      }

      // 音乐信息
      if (this.currentMusic) {
        Column() {
          Text(this.currentMusic.title)
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 8 })
          
          Text(this.currentMusic.artist)
            .fontSize(16)
            .fontColor('#888888')
        }
        .margin({ bottom: 30 })
      }

      // 进度条
      if (this.currentMusic) {
        Row() {
          Text(this.formatTime(this.playbackPosition / 1000))
            .fontSize(12)
            .width(50)
          
          Slider({
            value: this.playbackPosition,
            min: 0,
            max: this.currentMusic.duration,
            step: 1000,
            style: SliderStyle.OutSet
          })
            .layoutWeight(1)
            .onChange((value: number) => {
              this.localPlayer.seekTo(value);
              this.distService.syncPlaybackPosition(value);
            })
          
          Text(this.formatTime(this.currentMusic.duration / 1000))
            .fontSize(12)
            .width(50)
        }
        .width('90%')
        .margin({ bottom: 30 })
      }

      // 控制器(根据isControllerVisible显示/隐藏)
      if (this.isControllerVisible) {
        Row() {
          Button('上一首')
            .onClick(() => this.playPrevious())
          
          if (this.playbackState === PlaybackState.PLAYING) {
            Button('暂停')
              .margin({ left: 20, right: 20 })
              .onClick(() => {
                this.localPlayer.pause();
                this.distService.syncPlaybackState(PlaybackState.PAUSED);
              })
          } else {
            Button('播放')
              .margin({ left: 20, right: 20 })
              .onClick(() => {
                if (this.currentMusic) {
                  this.localPlayer.play(this.currentMusic);
                  this.distService.syncPlaybackState(PlaybackState.PLAYING);
                }
              })
          }
          
          Button('下一首')
            .onClick(() => this.playNext())
        }
        .margin({ bottom: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .onClick(() => {
      this.isControllerVisible = !this.isControllerVisible;
      this.resetControllerTimer();
    })
  }

  // 重置控制器隐藏计时器
  private resetControllerTimer() {
    if (this.controllerTimeout) {
      clearTimeout(this.controllerTimeout);
    }
    this.controllerTimeout = setTimeout(() => {
      this.isControllerVisible = false;
    }, 5000);
  }

  // 格式化时间显示
  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
  }

  // 播放上一首
  private playPrevious() {
    // 实现播放列表逻辑
  }

  // 播放下一首
  private playNext() {
    // 实现播放列表逻辑
  }

  onPageHide() {
    this.localPlayer.release();
  }
}

5. 音乐列表页面

// pages/MusicListPage.ets
import { LocalPlayerService } from '../service/LocalPlayerService';
import { DistributedMusicService } from '../service/DistributedMusicService';

@Entry
@Component
struct MusicListPage {
  private localPlayer: LocalPlayerService = new LocalPlayerService();
  private distService: DistributedMusicService = new DistributedMusicService();
  @State musicList: MusicItem[] = [];
  @StorageLink('currentMusic') currentMusic: MusicItem | null = null;

  async aboutToAppear() {
    await this.loadLocalMusic();
    await this.distService.init();
  }

  // 加载本地音乐
  async loadLocalMusic() {
    // 模拟加载本地音乐
    this.musicList = [
      new MusicItem({
        id: '1',
        title: '示例音乐1',
        artist: '艺术家1',
        duration: 180000,
        filePath: 'file://media/music1.mp3',
        coverUrl: $r('app.media.music_cover1')
      }),
      new MusicItem({
        id: '2',
        title: '示例音乐2',
        artist: '艺术家2',
        duration: 240000,
        filePath: 'file://media/music2.mp3',
        coverUrl: $r('app.media.music_cover2')
      })
    ];
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('我的音乐')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        
        Button('刷新')
          .onClick(() => this.loadLocalMusic())
      }
      .padding(16)
      .width('100%')

      // 音乐列表
      List() {
        ForEach(this.musicList, (music: MusicItem) => {
          ListItem() {
            MusicListItem({
              music: music,
              isCurrent: this.currentMusic?.id === music.id,
              onPlay: () => this.playMusic(music)
            })
          }
        })
      }
      .layoutWeight(1)
      .width('100%')
    }
    .width('100%')
    .height('100%')
  }

  // 播放音乐
  private async playMusic(music: MusicItem) {
    await this.localPlayer.initPlayer();
    await this.localPlayer.play(music);
    this.distService.syncCurrentMusic(music);
    this.distService.syncPlaybackState(PlaybackState.PLAYING);
    router.pushUrl({ url: 'pages/MusicPlayerPage' });
  }
}

@Component
struct MusicListItem {
  @Prop music: MusicItem;
  @Prop isCurrent: boolean;
  @Prop onPlay: () => void;

  build() {
    Row() {
      Image(this.music.coverUrl || $r('app.media.default_cover'))
        .width(50)
        .height(50)
        .margin({ right: 12 })
      
      Column() {
        Text(this.music.title)
          .fontSize(18)
          .fontColor(this.isCurrent ? '#FF5722' : '#000000')
        
        Text(this.music.artist)
          .fontSize(14)
          .fontColor('#888888')
      }
      .layoutWeight(1)
      
      Text(this.formatTime(this.music.duration / 1000))
        .fontSize(14)
        .fontColor('#888888')
    }
    .padding(12)
    .width('100%')
    .onClick(() => this.onPlay())
  }

  // 格式化时间显示
  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
  }
}

实现原理分析

  1. ​播放同步机制​​:

    • 主设备通过AVPlayer播放本地音乐
    • 播放状态变更时通过分布式数据服务同步到其他设备
    • 从设备接收到同步数据后更新本地UI和播放状态
  2. ​进度同步方案​​:

    • 主设备定期同步当前播放位置
    • 从设备显示同步的播放位置但不实际控制播放
    • 用户在主设备上拖动进度条时立即同步新位置
  3. ​设备协同逻辑​​:

    • 只有主设备实际执行播放操作
    • 从设备仅显示播放状态和进度
    • 用户在任何设备上的操作都会转移到主设备执行

扩展功能建议

  1. ​播放列表同步​​:

    // 同步播放列表
    async syncPlaylist(playlist: MusicItem[]) {
      await this.kvStore.put('music_playlist', JSON.stringify(playlist));
    }
  2. ​设备角色切换​​:

    // 切换主从设备
    async switchControllerRole(deviceId: string) {
      await this.kvStore.put('controller_device', deviceId);
    }
  3. ​跨设备传输音频​​:

    // 使用分布式文件服务传输音频文件
    async transferMusicToDevice(music: MusicItem, targetDevice: string) {
      const fileUri = await distributeFile(music.filePath, targetDevice);
      return fileUri;
    }

总结

本文展示了如何利用HarmonyOS的AVPlayer和分布式能力构建一个多设备协同的音乐播放器。通过本地播放服务实现高质量音频播放,再通过分布式数据管理实现播放状态和进度的多设备同步,为用户提供了无缝的跨设备音乐体验。

这种架构不仅适用于音乐播放器,也可以扩展到视频播放、播客等其他媒体应用场景。鸿蒙的分布式能力为开发者提供了强大的工具,使多设备协同开发变得更加简单高效。

Logo

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

更多推荐