鸿蒙跨端音乐同步:多设备协同音乐播放器
/ 音乐ID// 音乐标题// 艺术家// 专辑// 时长(毫秒)// 封面URL// 本地文件路径// 是否为远程文件: any) {// 播放状态枚举本文展示了如何利用HarmonyOS的AVPlayer和分布式能力构建一个多设备协同的音乐播放器。通过本地播放服务实现高质量音频播放,再通过分布式数据管理实现播放状态和进度的多设备同步,为用户提供了无缝的跨设备音乐体验。这种架构不仅适用于音乐播放
·
鸿蒙跨端音乐同步:多设备协同音乐播放器
本文将基于HarmonyOS 5的AVPlayer和分布式能力,实现一个支持多设备协同的音乐播放器,可以在不同设备间同步播放状态、播放列表和播放进度。
技术架构
- 媒体播放层:使用
@ohos.multimedia.media的AVPlayer实现本地播放 - 数据同步层:通过分布式数据管理实现播放状态同步
- 设备管理层:发现和连接同一账号下的其他设备
- 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}`;
}
}
实现原理分析
-
播放同步机制:
- 主设备通过AVPlayer播放本地音乐
- 播放状态变更时通过分布式数据服务同步到其他设备
- 从设备接收到同步数据后更新本地UI和播放状态
-
进度同步方案:
- 主设备定期同步当前播放位置
- 从设备显示同步的播放位置但不实际控制播放
- 用户在主设备上拖动进度条时立即同步新位置
-
设备协同逻辑:
- 只有主设备实际执行播放操作
- 从设备仅显示播放状态和进度
- 用户在任何设备上的操作都会转移到主设备执行
扩展功能建议
-
播放列表同步:
// 同步播放列表 async syncPlaylist(playlist: MusicItem[]) { await this.kvStore.put('music_playlist', JSON.stringify(playlist)); } -
设备角色切换:
// 切换主从设备 async switchControllerRole(deviceId: string) { await this.kvStore.put('controller_device', deviceId); } -
跨设备传输音频:
// 使用分布式文件服务传输音频文件 async transferMusicToDevice(music: MusicItem, targetDevice: string) { const fileUri = await distributeFile(music.filePath, targetDevice); return fileUri; }
总结
本文展示了如何利用HarmonyOS的AVPlayer和分布式能力构建一个多设备协同的音乐播放器。通过本地播放服务实现高质量音频播放,再通过分布式数据管理实现播放状态和进度的多设备同步,为用户提供了无缝的跨设备音乐体验。
这种架构不仅适用于音乐播放器,也可以扩展到视频播放、播客等其他媒体应用场景。鸿蒙的分布式能力为开发者提供了强大的工具,使多设备协同开发变得更加简单高效。
更多推荐
所有评论(0)