HarmonyOS Sample 之 DistributedMusicPlayer分布式音樂(lè)播放器
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
DistributedMusicPlayer分布式音樂(lè)播放器
介紹
本示例主要演示了如何通過(guò)遷移數(shù)據(jù)進(jìn)行音樂(lè)的分布式播放。實(shí)現(xiàn)了音樂(lè)播放的跨設(shè)備遷移,包括:播放哪首歌曲、播放進(jìn)度、以及播放狀態(tài)的保持。
效果展示
搭建環(huán)境
安裝DevEco Studio,詳情請(qǐng)參考DevEco Studio下載。
設(shè)置DevEco Studio開(kāi)發(fā)環(huán)境,DevEco Studio開(kāi)發(fā)環(huán)境需要依賴(lài)于網(wǎng)絡(luò)環(huán)境,需要連接上網(wǎng)絡(luò)才能確保工具的正常使用,可以根據(jù)如下兩種情況來(lái)配置開(kāi)發(fā)環(huán)境:
如果可以直接訪(fǎng)問(wèn)Internet,只需進(jìn)行下載HarmonyOS SDK操作。
如果網(wǎng)絡(luò)不能直接訪(fǎng)問(wèn)Internet,需要通過(guò)代理服務(wù)器才可以訪(fǎng)問(wèn),請(qǐng)參考配置開(kāi)發(fā)環(huán)境。
下載源碼,導(dǎo)入項(xiàng)目。
代碼結(jié)構(gòu)
- config.json #全局配置文件
- │
- ├─java
- │ └─ohos
- │ └─samples
- │ └─distributedmusicplayer
- │ │ MainAbility.java
- │ │
- │ ├─slice
- │ │ MainAbilitySlice.java #播放器主能力Slice
- │ │
- │ └─utils
- │ LogUtil.java #日志工具類(lèi)
- │ PlayerManager.java #播放器管理者
- │ PlayerStateListener.java #播放器狀態(tài)監(jiān)聽(tīng)器
- │
- └─resources
- ├─base
- │ ├─element
- │ │ string.json
- │ │
- │ ├─graphic
- │ │ button_bg.xml
- │ │
- │ ├─layout
- │ │ main_ability_slice.xml #播放器頁(yè)面布局
- │ │
- │ └─media #海報(bào)、按鈕圖片資源
- │ album.png
- │ album2.png
- │ bg_blurry.png
- │ icon.png
- │ ic_himusic_next.png
- │ ic_himusic_pause.png
- │ ic_himusic_play.png
- │ ic_himusic_previous.png
- │ remote_play_selected.png
- │
- └─rawfile #歌曲媒體資源
- Homey.mp3
- Homey.wav
- Technology.mp3
- Technology.wav
實(shí)現(xiàn)步驟
1.實(shí)現(xiàn)跨設(shè)備遷移標(biāo)準(zhǔn)步驟,參見(jiàn)HarmonyOS Sample 之 AbilityInteraction設(shè)備遷移
2.實(shí)現(xiàn)一個(gè)播放器管理者PlayerManager
2.1.定義播放器的狀態(tài),包括: 播放、暫停、完成、播放中
- private static final int PLAY_STATE_PLAY = 0x0000001;
- private static final int PLAY_STATE_PAUSE = 0x0000002;
- private static final int PLAY_STATE_FINISH = 0x0000003;
- private static final int PLAY_STATE_PROGRESS = 0x0000004;
2.2.實(shí)現(xiàn)基本的方法,包括:播放、暫停、切換歌曲、更新播放進(jìn)度方法
還有一些輔助方法,包括:設(shè)置媒體資源、定時(shí)更新播放進(jìn)度、獲取播放總時(shí)長(zhǎng)、
要用到Player/Timer/自定義的PlayerStateListener/EventHandler事件處理/PlayCallBack播放器回調(diào)類(lèi)
- /**
- * play
- */
- public void play() {
- try {
- if (!isPrepared) {
- LogUtil.error(TAG, "prepare fail");
- return;
- }
- //如果開(kāi)始播放則返回真; 否則返回 false。
- if (!musicPlayer.play()) {
- LogUtil.error(TAG, "play fail");
- return;
- }
- startTask();
- handler.sendEvent(PLAY_STATE_PLAY);
- } catch (IllegalArgumentException e) {
- LogUtil.error(TAG, e.getMessage());
- e.printStackTrace();
- }
- }
- /**
- * pause
- */
- public void pause() {
- if (!musicPlayer.pause()) {
- LogUtil.info(TAG, "pause fail");
- return;
- }
- //停止計(jì)時(shí)
- finishTask();
- //
- handler.sendEvent(PLAY_STATE_PAUSE);
- }
- /**
- * switch music
- *
- * @param uri music uri
- */
- public void switchMusic(String uri) {
- currentUri = uri;
- //設(shè)置資源
- setResource(currentUri);
- //播放
- play();
- }
- /**
- * changes the playback position
- * 更新當(dāng)前播放進(jìn)度
- *
- * @param currentTime current time
- */
- public void rewindTo(int currentTime) {
- musicPlayer.rewindTo(currentTime * 1000);
- }
- /**
- * set source
- *
- * @param uri music uri
- */
- public void setResource(String uri) {
- LogUtil.info(TAG, "setResource,uri: " + uri);
- try {
- RawFileEntry rawFileEntry = context.getResourceManager().getRawFileEntry(uri);
- BaseFileDescriptor baseFileDescriptor = rawFileEntry.openRawFileDescriptor();
- //LogUtil.info(TAG, "setResource,baseFileDescriptor : " + baseFileDescriptor);
- if (!musicPlayer.setSource(baseFileDescriptor)) {
- LogUtil.info(TAG, "uri is invalid");
- return;
- }
- //準(zhǔn)備播放環(huán)境并緩沖媒體數(shù)據(jù)。
- isPrepared = musicPlayer.prepare();
- LogUtil.info(TAG, "setResource,isPrepared: " + isPrepared);
- //歌曲名稱(chēng)
- String listenerUri = currentUri.substring(currentUri.lastIndexOf("/") + 1, currentUri.lastIndexOf("."));
- playerStateListener.onUriSet(listenerUri);
- LogUtil.info(TAG, "setResource,listenerUri: " + listenerUri);
- } catch (IOException e) {
- LogUtil.error(TAG, "io exception");
- }
- }
- /**
- * 定時(shí)事件通知更新進(jìn)度條
- */
- private void startTask() {
- LogUtil.debug(TAG, "startTask");
- finishTask();
- timerTask = new TimerTask() {
- @Override
- public void run() {
- handler.sendEvent(PLAY_STATE_PROGRESS);
- }
- };
- timer = new Timer();
- timer.schedule(timerTask, DELAY_TIME, PERIOD);
- }
- private void finishTask() {
- LogUtil.debug(TAG, "finishTask");
- if (timer != null && timerTask != null) {
- timer.cancel();
- timer = null;
- timerTask = null;
- }
- }
2.3.PlayerStateListener播放器狀態(tài)監(jiān)聽(tīng)器有如下方法:
onPlaySuccess播放成功時(shí)被調(diào)用
onPauseSuccess暫停時(shí)被調(diào)用
onPositionChange進(jìn)度發(fā)生變化時(shí)被調(diào)用
onMusicFinished音樂(lè)播放完成時(shí)被調(diào)用
onUriSet資源被設(shè)置時(shí)被調(diào)用
- /**
- * PlayerStateListener
- */
- public interface PlayerStateListener {
- void onPlaySuccess(int totalTime);
- void onPauseSuccess();
- void onPositionChange(int currentTime);
- void onMusicFinished();
- void onUriSet(String name);
- }
2.4.PlayCallBack播放器回調(diào)類(lèi)實(shí)現(xiàn)了Player.IPlayerCallback接口,實(shí)現(xiàn)了如下方法:
onPrepared 當(dāng)媒體文件準(zhǔn)備好播放時(shí)調(diào)用。
onMessage當(dāng)收到播放器消息或警報(bào)時(shí)調(diào)用。
onError收到播放器錯(cuò)誤消息時(shí)調(diào)用。
onResolutionChanged當(dāng)視頻大小改變時(shí)調(diào)用。
onPlayBackComplete播放完成時(shí)調(diào)用。
onRewindToComplete 當(dāng)播放位置被 Player.rewindTo(long) 改變時(shí)調(diào)用。
onBufferingChange當(dāng)緩沖百分比更新時(shí)調(diào)用。
onNewTimedMetaData當(dāng)有新的定時(shí)元數(shù)據(jù)可用時(shí)調(diào)用。
onMediaTimeIncontinuity當(dāng)媒體時(shí)間連續(xù)性中斷時(shí)調(diào)用,例如播放過(guò)程中出現(xiàn)錯(cuò)誤,播放位置被Player.rewindTo(long)改變,或者播放速度突然改變。
- /**
- * 在播放完成、播放位置更改和視頻大小更改時(shí)提供媒體播放器回調(diào)。
- */
- private class PlayCallBack implements Player.IPlayerCallback {
- /**
- * 當(dāng)媒體文件準(zhǔn)備好播放時(shí)調(diào)用。
- */
- @Override
- public void onPrepared() {
- LogUtil.info(TAG, "onPrepared");
- }
- /**
- * 當(dāng)收到播放器消息或警報(bào)時(shí)調(diào)用。
- *
- * @param type
- * @param extra
- */
- @Override
- public void onMessage(int type, int extra) {
- LogUtil.info(TAG, "onMessage " + type + "-" + extra);
- }
- /**
- * 收到播放器錯(cuò)誤消息時(shí)調(diào)用。
- *
- * @param errorType
- * @param errorCode
- */
- @Override
- public void onError(int errorType, int errorCode) {
- LogUtil.info(TAG, "onError " + errorType + "-" + errorCode);
- }
- /**
- * 當(dāng)視頻大小改變時(shí)調(diào)用。
- *
- * @param width
- * @param height
- */
- @Override
- public void onResolutionChanged(int width, int height) {
- LogUtil.info(TAG, "onResolutionChanged " + width + "-" + height);
- }
- /**
- * 播放完成時(shí)調(diào)用。
- */
- @Override
- public void onPlayBackComplete() {
- //不會(huì)自動(dòng)被調(diào)用????
- LogUtil.info(TAG, "onPlayBackComplete----------------");
- handler.sendEvent(PLAY_STATE_FINISH);
- }
- /**
- * 當(dāng)播放位置被 Player.rewindTo(long) 改變時(shí)調(diào)用。
- */
- @Override
- public void onRewindToComplete() {
- LogUtil.info(TAG, "onRewindToComplete");
- }
- /**
- * 當(dāng)緩沖百分比更新時(shí)調(diào)用。
- *
- * @param percent
- */
- @Override
- public void onBufferingChange(int percent) {
- LogUtil.info(TAG, "onBufferingChange:" + percent);
- }
- /**
- * 當(dāng)有新的定時(shí)元數(shù)據(jù)可用時(shí)調(diào)用。
- *
- * @param mediaTimedMetaData
- */
- @Override
- public void onNewTimedMetaData(Player.MediaTimedMetaData mediaTimedMetaData) {
- LogUtil.info(TAG, "onNewTimedMetaData");
- }
- /**
- * 當(dāng)媒體時(shí)間連續(xù)性中斷時(shí)調(diào)用,例如播放過(guò)程中出現(xiàn)錯(cuò)誤,播放位置被Player.rewindTo(long)改變,或者播放速度突然改變。
- *
- * @param mediaTimeInfo
- */
- @Override
- public void onMediaTimeIncontinuity(Player.MediaTimeInfo mediaTimeInfo) {
- LogUtil.info(TAG, "onNewTimedMetaData");
- }
- }
3.MainAbilitySlice 中 implements PlayerStateListener , IAbilityContinuation接口
- public class MainAbilitySlice extends AbilitySlice implements PlayerStateListener, IAbilityContinuation {
- ...
3.1.實(shí)現(xiàn)PlayerStateListener接口方法
- @Override
- public void onPlaySuccess(int totalTime) {
- LogUtil.debug(TAG, "onPlaySuccess");
- //設(shè)置圖標(biāo)
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_pause);
- //設(shè)置總時(shí)長(zhǎng)文本
- this.totalTimeText.setText(getTime(totalTime));
- //設(shè)置進(jìn)度條
- slider.setMaxValue(totalTime);
- //設(shè)置當(dāng)前歌曲海報(bào)
- musicPosters.setPixelMap(posters[currentPos]);
- }
- @Override
- public void onPauseSuccess() {
- LogUtil.debug(TAG, "onPauseSuccess");
- //設(shè)置圖標(biāo)
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_play);
- }
- @Override
- public void onUriSet(String name) {
- LogUtil.debug(TAG, "onUriSet");
- //設(shè)置歌曲名稱(chēng)
- musicNameText.setText(name);
- }
- @Override
- public void onPositionChange(int currentTime) {
- if(currentTime < totalTime){
- LogUtil.info(TAG, "onPositionChange currentTime = " + currentTime+",totalTime="+totalTime);
- this.currentTime = currentTime;
- //設(shè)置播放時(shí)間文本
- this.currentTimeText.setText(getTime(currentTime));
- //設(shè)置進(jìn)度條的當(dāng)前播放時(shí)間
- slider.setProgressValue(currentTime);
- }else{
- LogUtil.info(TAG, "onPositionChange, current song end");
- //設(shè)置播放器圖標(biāo)
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_play);
- }
- }
- /**
- *音樂(lè)播放完成時(shí)應(yīng)該被調(diào)用,但是沒(méi)被調(diào)用
- */
- @Override
- public void onMusicFinished() {
- //TODO???????????
- LogUtil.debug(TAG, "onMusicFinished");
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //切換歌曲
- playerManager.switchMusic(currentUri);
- //總時(shí)長(zhǎng)
- totalTime=playerManager.getTotalTime();
- }
3.2.實(shí)現(xiàn)IAbilityContinuation接口方法
- @Override
- public boolean onStartContinuation() {
- LogUtil.debug(TAG, "onStartContinuation");
- return true;
- }
- @Override
- public boolean onSaveData(IntentParams intentParams) {
- LogUtil.debug(TAG, "onSaveData");
- //
- intentParams.setParam(KEY_CURRENT_TIME, currentTime);
- intentParams.setParam(KEY_POSITION, currentPos);
- intentParams.setParam(KEY_PLAY_STATE, String.valueOf(playerManager.isPlaying()));
- LogUtil.info(TAG, "onSaveData:" + currentTime);
- return true;
- }
- @Override
- public boolean onRestoreData(IntentParams intentParams) {
- LogUtil.debug(TAG, "onRestoreData");
- if (!(intentParams.getParam(KEY_POSITION) instanceof Integer)) {
- return false;
- }
- if (!(intentParams.getParam(KEY_CURRENT_TIME) instanceof Integer)) {
- return false;
- }
- if (!(intentParams.getParam(KEY_PLAY_STATE) instanceof String)) {
- return false;
- }
- //恢復(fù)數(shù)據(jù),獲取遷移過(guò)來(lái)的參數(shù):播放位置、時(shí)間和播放狀態(tài)
- currentPos = (int) intentParams.getParam(KEY_POSITION);
- currentTime = (int) intentParams.getParam(KEY_CURRENT_TIME);
- Object object = intentParams.getParam(KEY_PLAY_STATE);
- if (object instanceof String) {
- isPlaying = Boolean.parseBoolean((String) object);
- }
- isInteractionPlay = true;
- LogUtil.info(TAG, "onRestoreData:" + currentTime);
- return true;
- }
- @Override
- public void onCompleteContinuation(int i) {
- terminate();
- }
3.3.定義ValueChangedListenerImpl進(jìn)度值變化的監(jiān)聽(tīng)事件
實(shí)現(xiàn) Slider.ValueChangedListener 接口方法
- /**
- *進(jìn)度條值變化的監(jiān)聽(tīng)事件
- */
- private class ValueChangedListenerImpl implements Slider.ValueChangedListener {
- @Override
- public void onProgressUpdated(Slider slider, int progress, boolean fromUser) {
- currentTime = progress;
- }
- @Override
- public void onTouchStart(Slider slider) {
- LogUtil.debug(TAG, "onTouchStart");
- }
- @Override
- public void onTouchEnd(Slider slider) {
- LogUtil.debug(TAG, "onTouchEnd");
- //快速更改播放進(jìn)度
- playerManager.rewindTo(currentTime);
- //當(dāng)前播放時(shí)間
- currentTimeText.setText(getTime(currentTime));
- }
- }
3.4.定義遷移數(shù)據(jù)的KEY,音樂(lè)當(dāng)前的播放時(shí)間、播放的歌曲索引(位置)、播放狀態(tài)
- private static final String KEY_CURRENT_TIME = "main_ability_slice_current_time";
- private static final String KEY_POSITION = "main_ability_slice_position";
- private static final String KEY_PLAY_STATE = "main_ability_slice_play_state";
- private int currentPos = 0;
- private String currentUri;
- //是否是互動(dòng)播放,true表示遠(yuǎn)端遷移恢復(fù)的
- private boolean isInteractionPlay;
- private int currentTime;
- //當(dāng)前播放歌曲總時(shí)長(zhǎng)
- private int totalTime;
- private boolean isPlaying;
3.5.定義播放的音樂(lè)URI,這里準(zhǔn)備了2首,還有對(duì)應(yīng)的海報(bào)
- private static final String URI1 = "resources/rawfile/Technology.wav";
- private static final String URI2 = "resources/rawfile/Homey.wav";
- private final String[] musics = {URI1, URI2};
- private final int[] posters = {ResourceTable.Media_album, ResourceTable.Media_album2};
3.6.onStart完成數(shù)據(jù)的初始化
- @Override
- public void onStart(Intent intent) {
- super.onStart(intent);
- super.setUIContent(ResourceTable.Layout_main_ability_slice);
- initComponents();
- initMedia();
- updateUI();
- }
初始化界面組件,實(shí)現(xiàn)對(duì)應(yīng)按鈕的監(jiān)聽(tīng)事件
播放或暫停、上一首、下一首、遷移以及進(jìn)度條的進(jìn)度變化事件的監(jiān)聽(tīng)
- /**
- * 初始化界面組件,實(shí)現(xiàn)對(duì)應(yīng)按鈕的監(jiān)聽(tīng)事件
- * 播放或暫停、上一首、下一首、遷移以及進(jìn)度條的進(jìn)度變化事件的監(jiān)聽(tīng)
- */
- private void initComponents() {
- LogUtil.debug(TAG, "initComponents");
- musicNameText = (Text) findComponentById(ResourceTable.Id_music_name);
- currentTimeText = (Text) findComponentById(ResourceTable.Id_play_progress_time);
- totalTimeText = (Text) findComponentById(ResourceTable.Id_play_total_time);
- musicPosters = (Image) findComponentById(ResourceTable.Id_music_posters);
- musicPlayButton = (Image) findComponentById(ResourceTable.Id_music_play_btn);
- findComponentById(ResourceTable.Id_remote_play).setClickedListener(this::continueAbility);
- findComponentById(ResourceTable.Id_music_play_prev_btn).setClickedListener(this::prevMusic);
- findComponentById(ResourceTable.Id_music_play_next_btn).setClickedListener(this::nextMusic);
- musicPlayButton.setClickedListener(this::playOrPauseMusic);
- //
- slider = (Slider) findComponentById(ResourceTable.Id_play_progress_bar);
- slider.setValueChangedListener(new ValueChangedListenerImpl());
- }
- private void continueAbility(Component component) {
- try {
- continueAbility();
- } catch (IllegalStateException e) {
- LogUtil.info(TAG, e.getMessage());
- }
- }
- /**
- * 上一首
- * @param component
- */
- private void prevMusic(Component component) {
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //
- playerManager.switchMusic(currentUri);
- //總時(shí)長(zhǎng)
- totalTime=playerManager.getTotalTime();
- }
- /**
- * 下一首
- * @param component
- */
- private void nextMusic(Component component) {
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //切換音樂(lè)
- playerManager.switchMusic(currentUri);
- //總時(shí)長(zhǎng)
- totalTime=playerManager.getTotalTime();
- }
- /**
- * 播放或暫停音樂(lè)
- * @param component
- */
- private void playOrPauseMusic(Component component) {
- //
- playOrPause();
- }
- /**
- * 播放或暫停
- */
- private void playOrPause() {
- LogUtil.debug(TAG, "playOrPause,playerManager:"+playerManager);
- try {
- //
- if (playerManager.isPlaying()) {
- LogUtil.debug(TAG, "playOrPause pause");
- playerManager.pause();
- }else{
- //設(shè)置資源
- playerManager.setResource(currentUri);
- //設(shè)置進(jìn)度
- playerManager.rewindTo(currentTime);
- playerManager.play();
- LogUtil.debug(TAG, "playOrPause play");
- }
- } catch (Exception e) {
- LogUtil.error(TAG, "playOrPause");
- e.printStackTrace();
- }
- }
3.7.初始化媒體對(duì)象
當(dāng)前播放歌曲資源,播放器管理者
- /**
- * 初始化媒體對(duì)象
- * 當(dāng)前播放歌曲資源
- * 播放器管理者
- */
- private void initMedia() {
- LogUtil.debug(TAG, "initMedia");
- //當(dāng)前媒體URI
- currentUri = musics[currentPos];
- LogUtil.debug(TAG, "initMedia,currentUri:"+currentUri);
- //初始化playerManager
- playerManager = new PlayerManager(getApplicationContext(), currentUri);
- //弱引用對(duì)象,不會(huì)阻止它們的引用對(duì)象被終結(jié)、終結(jié)和回收。 弱引用最常用于實(shí)現(xiàn)規(guī)范化映射。
- WeakReference<PlayerStateListener> playerStateListener = new WeakReference<>(this);
- //設(shè)置狀態(tài)監(jiān)聽(tīng)器
- playerManager.setPlayerStateListener(playerStateListener.get());
- //初始化播放器信息
- playerManager.init();
- LogUtil.debug(TAG, "initMedia FINISH");
- }
3.8.遠(yuǎn)端遷移后恢復(fù)播放界面
恢復(fù)播放器的播放進(jìn)度、播放狀態(tài)、海報(bào)、當(dāng)前時(shí)間和總時(shí)長(zhǎng)、slider播放進(jìn)度
- /**
- * 遠(yuǎn)端遷移后恢復(fù)的播放,恢復(fù)播放器的播放進(jìn)度
- * 更新UI界面
- */
- private void updateUI() {
- LogUtil.debug(TAG, "updateUI");
- //海報(bào)
- musicPosters.setPixelMap(posters[currentPos]);
- //當(dāng)前時(shí)間和總時(shí)長(zhǎng)
- currentTimeText.setText(getTime(currentTime));
- totalTimeText.setText(getTime(playerManager.getTotalTime()));
- //播放進(jìn)度
- slider.setMaxValue(playerManager.getTotalTime());
- slider.setProgressValue(currentTime);
- //總時(shí)長(zhǎng)
- totalTime=playerManager.getTotalTime();
- //遠(yuǎn)端遷移恢復(fù)
- if (isInteractionPlay) {
- LogUtil.debug(TAG, "remotePlay,rewindTo:"+currentTime);
- playerManager.rewindTo(currentTime);
- if (!isPlaying) {
- return;
- }
- //播放
- playerManager.play();
- }
- }
問(wèn)題總結(jié)
1.onMusicFinished 音樂(lè)播放完成時(shí)應(yīng)該被調(diào)用,但是多數(shù)沒(méi)被調(diào)用,只是偶爾會(huì)調(diào)用,難道是我電腦性能跟不上了?
2.優(yōu)化了源碼中應(yīng)用啟動(dòng)后,點(diǎn)擊播放無(wú)法播放的問(wèn)題
3.優(yōu)化了播放器播放完當(dāng)前歌曲更新播放圖標(biāo)
4.增加了相關(guān)的注釋說(shuō)明
附件直接下載DistributedMusicPlayer.zip
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)