HarmonyOS Sample 之 DistributedMusicPlayer分布式音樂播放器
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
DistributedMusicPlayer分布式音樂播放器
介紹
本示例主要演示了如何通過遷移數(shù)據(jù)進行音樂的分布式播放。實現(xiàn)了音樂播放的跨設備遷移,包括:播放哪首歌曲、播放進度、以及播放狀態(tài)的保持。
效果展示
搭建環(huán)境
安裝DevEco Studio,詳情請參考DevEco Studio下載。
設置DevEco Studio開發(fā)環(huán)境,DevEco Studio開發(fā)環(huán)境需要依賴于網(wǎng)絡環(huán)境,需要連接上網(wǎng)絡才能確保工具的正常使用,可以根據(jù)如下兩種情況來配置開發(fā)環(huán)境:
如果可以直接訪問Internet,只需進行下載HarmonyOS SDK操作。
如果網(wǎng)絡不能直接訪問Internet,需要通過代理服務器才可以訪問,請參考配置開發(fā)環(huán)境。
下載源碼,導入項目。
代碼結(jié)構(gòu)
- config.json #全局配置文件
- │
- ├─java
- │ └─ohos
- │ └─samples
- │ └─distributedmusicplayer
- │ │ MainAbility.java
- │ │
- │ ├─slice
- │ │ MainAbilitySlice.java #播放器主能力Slice
- │ │
- │ └─utils
- │ LogUtil.java #日志工具類
- │ PlayerManager.java #播放器管理者
- │ PlayerStateListener.java #播放器狀態(tài)監(jiān)聽器
- │
- └─resources
- ├─base
- │ ├─element
- │ │ string.json
- │ │
- │ ├─graphic
- │ │ button_bg.xml
- │ │
- │ ├─layout
- │ │ main_ability_slice.xml #播放器頁面布局
- │ │
- │ └─media #海報、按鈕圖片資源
- │ 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
實現(xiàn)步驟
1.實現(xiàn)跨設備遷移標準步驟,參見HarmonyOS Sample 之 AbilityInteraction設備遷移
2.實現(xiàn)一個播放器管理者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.實現(xiàn)基本的方法,包括:播放、暫停、切換歌曲、更新播放進度方法
還有一些輔助方法,包括:設置媒體資源、定時更新播放進度、獲取播放總時長、
要用到Player/Timer/自定義的PlayerStateListener/EventHandler事件處理/PlayCallBack播放器回調(diào)類
- /**
- * play
- */
- public void play() {
- try {
- if (!isPrepared) {
- LogUtil.error(TAG, "prepare fail");
- return;
- }
- //如果開始播放則返回真; 否則返回 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;
- }
- //停止計時
- finishTask();
- //
- handler.sendEvent(PLAY_STATE_PAUSE);
- }
- /**
- * switch music
- *
- * @param uri music uri
- */
- public void switchMusic(String uri) {
- currentUri = uri;
- //設置資源
- setResource(currentUri);
- //播放
- play();
- }
- /**
- * changes the playback position
- * 更新當前播放進度
- *
- * @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;
- }
- //準備播放環(huán)境并緩沖媒體數(shù)據(jù)。
- isPrepared = musicPlayer.prepare();
- LogUtil.info(TAG, "setResource,isPrepared: " + isPrepared);
- //歌曲名稱
- 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");
- }
- }
- /**
- * 定時事件通知更新進度條
- */
- 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)聽器有如下方法:
onPlaySuccess播放成功時被調(diào)用
onPauseSuccess暫停時被調(diào)用
onPositionChange進度發(fā)生變化時被調(diào)用
onMusicFinished音樂播放完成時被調(diào)用
onUriSet資源被設置時被調(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)類實現(xiàn)了Player.IPlayerCallback接口,實現(xiàn)了如下方法:
onPrepared 當媒體文件準備好播放時調(diào)用。
onMessage當收到播放器消息或警報時調(diào)用。
onError收到播放器錯誤消息時調(diào)用。
onResolutionChanged當視頻大小改變時調(diào)用。
onPlayBackComplete播放完成時調(diào)用。
onRewindToComplete 當播放位置被 Player.rewindTo(long) 改變時調(diào)用。
onBufferingChange當緩沖百分比更新時調(diào)用。
onNewTimedMetaData當有新的定時元數(shù)據(jù)可用時調(diào)用。
onMediaTimeIncontinuity當媒體時間連續(xù)性中斷時調(diào)用,例如播放過程中出現(xiàn)錯誤,播放位置被Player.rewindTo(long)改變,或者播放速度突然改變。
- /**
- * 在播放完成、播放位置更改和視頻大小更改時提供媒體播放器回調(diào)。
- */
- private class PlayCallBack implements Player.IPlayerCallback {
- /**
- * 當媒體文件準備好播放時調(diào)用。
- */
- @Override
- public void onPrepared() {
- LogUtil.info(TAG, "onPrepared");
- }
- /**
- * 當收到播放器消息或警報時調(diào)用。
- *
- * @param type
- * @param extra
- */
- @Override
- public void onMessage(int type, int extra) {
- LogUtil.info(TAG, "onMessage " + type + "-" + extra);
- }
- /**
- * 收到播放器錯誤消息時調(diào)用。
- *
- * @param errorType
- * @param errorCode
- */
- @Override
- public void onError(int errorType, int errorCode) {
- LogUtil.info(TAG, "onError " + errorType + "-" + errorCode);
- }
- /**
- * 當視頻大小改變時調(diào)用。
- *
- * @param width
- * @param height
- */
- @Override
- public void onResolutionChanged(int width, int height) {
- LogUtil.info(TAG, "onResolutionChanged " + width + "-" + height);
- }
- /**
- * 播放完成時調(diào)用。
- */
- @Override
- public void onPlayBackComplete() {
- //不會自動被調(diào)用????
- LogUtil.info(TAG, "onPlayBackComplete----------------");
- handler.sendEvent(PLAY_STATE_FINISH);
- }
- /**
- * 當播放位置被 Player.rewindTo(long) 改變時調(diào)用。
- */
- @Override
- public void onRewindToComplete() {
- LogUtil.info(TAG, "onRewindToComplete");
- }
- /**
- * 當緩沖百分比更新時調(diào)用。
- *
- * @param percent
- */
- @Override
- public void onBufferingChange(int percent) {
- LogUtil.info(TAG, "onBufferingChange:" + percent);
- }
- /**
- * 當有新的定時元數(shù)據(jù)可用時調(diào)用。
- *
- * @param mediaTimedMetaData
- */
- @Override
- public void onNewTimedMetaData(Player.MediaTimedMetaData mediaTimedMetaData) {
- LogUtil.info(TAG, "onNewTimedMetaData");
- }
- /**
- * 當媒體時間連續(xù)性中斷時調(diào)用,例如播放過程中出現(xiàn)錯誤,播放位置被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.實現(xiàn)PlayerStateListener接口方法
- @Override
- public void onPlaySuccess(int totalTime) {
- LogUtil.debug(TAG, "onPlaySuccess");
- //設置圖標
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_pause);
- //設置總時長文本
- this.totalTimeText.setText(getTime(totalTime));
- //設置進度條
- slider.setMaxValue(totalTime);
- //設置當前歌曲海報
- musicPosters.setPixelMap(posters[currentPos]);
- }
- @Override
- public void onPauseSuccess() {
- LogUtil.debug(TAG, "onPauseSuccess");
- //設置圖標
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_play);
- }
- @Override
- public void onUriSet(String name) {
- LogUtil.debug(TAG, "onUriSet");
- //設置歌曲名稱
- musicNameText.setText(name);
- }
- @Override
- public void onPositionChange(int currentTime) {
- if(currentTime < totalTime){
- LogUtil.info(TAG, "onPositionChange currentTime = " + currentTime+",totalTime="+totalTime);
- this.currentTime = currentTime;
- //設置播放時間文本
- this.currentTimeText.setText(getTime(currentTime));
- //設置進度條的當前播放時間
- slider.setProgressValue(currentTime);
- }else{
- LogUtil.info(TAG, "onPositionChange, current song end");
- //設置播放器圖標
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_play);
- }
- }
- /**
- *音樂播放完成時應該被調(diào)用,但是沒被調(diào)用
- */
- @Override
- public void onMusicFinished() {
- //TODO???????????
- LogUtil.debug(TAG, "onMusicFinished");
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //切換歌曲
- playerManager.switchMusic(currentUri);
- //總時長
- totalTime=playerManager.getTotalTime();
- }
3.2.實現(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;
- }
- //恢復數(shù)據(jù),獲取遷移過來的參數(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進度值變化的監(jiān)聽事件
實現(xiàn) Slider.ValueChangedListener 接口方法
- /**
- *進度條值變化的監(jiān)聽事件
- */
- 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");
- //快速更改播放進度
- playerManager.rewindTo(currentTime);
- //當前播放時間
- currentTimeText.setText(getTime(currentTime));
- }
- }
3.4.定義遷移數(shù)據(jù)的KEY,音樂當前的播放時間、播放的歌曲索引(位置)、播放狀態(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;
- //是否是互動播放,true表示遠端遷移恢復的
- private boolean isInteractionPlay;
- private int currentTime;
- //當前播放歌曲總時長
- private int totalTime;
- private boolean isPlaying;
3.5.定義播放的音樂URI,這里準備了2首,還有對應的海報
- 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();
- }
初始化界面組件,實現(xiàn)對應按鈕的監(jiān)聽事件
播放或暫停、上一首、下一首、遷移以及進度條的進度變化事件的監(jiān)聽
- /**
- * 初始化界面組件,實現(xiàn)對應按鈕的監(jiān)聽事件
- * 播放或暫停、上一首、下一首、遷移以及進度條的進度變化事件的監(jiān)聽
- */
- 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);
- //總時長
- totalTime=playerManager.getTotalTime();
- }
- /**
- * 下一首
- * @param component
- */
- private void nextMusic(Component component) {
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //切換音樂
- playerManager.switchMusic(currentUri);
- //總時長
- totalTime=playerManager.getTotalTime();
- }
- /**
- * 播放或暫停音樂
- * @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{
- //設置資源
- playerManager.setResource(currentUri);
- //設置進度
- playerManager.rewindTo(currentTime);
- playerManager.play();
- LogUtil.debug(TAG, "playOrPause play");
- }
- } catch (Exception e) {
- LogUtil.error(TAG, "playOrPause");
- e.printStackTrace();
- }
- }
3.7.初始化媒體對象
當前播放歌曲資源,播放器管理者
- /**
- * 初始化媒體對象
- * 當前播放歌曲資源
- * 播放器管理者
- */
- private void initMedia() {
- LogUtil.debug(TAG, "initMedia");
- //當前媒體URI
- currentUri = musics[currentPos];
- LogUtil.debug(TAG, "initMedia,currentUri:"+currentUri);
- //初始化playerManager
- playerManager = new PlayerManager(getApplicationContext(), currentUri);
- //弱引用對象,不會阻止它們的引用對象被終結(jié)、終結(jié)和回收。 弱引用最常用于實現(xiàn)規(guī)范化映射。
- WeakReference<PlayerStateListener> playerStateListener = new WeakReference<>(this);
- //設置狀態(tài)監(jiān)聽器
- playerManager.setPlayerStateListener(playerStateListener.get());
- //初始化播放器信息
- playerManager.init();
- LogUtil.debug(TAG, "initMedia FINISH");
- }
3.8.遠端遷移后恢復播放界面
恢復播放器的播放進度、播放狀態(tài)、海報、當前時間和總時長、slider播放進度
- /**
- * 遠端遷移后恢復的播放,恢復播放器的播放進度
- * 更新UI界面
- */
- private void updateUI() {
- LogUtil.debug(TAG, "updateUI");
- //海報
- musicPosters.setPixelMap(posters[currentPos]);
- //當前時間和總時長
- currentTimeText.setText(getTime(currentTime));
- totalTimeText.setText(getTime(playerManager.getTotalTime()));
- //播放進度
- slider.setMaxValue(playerManager.getTotalTime());
- slider.setProgressValue(currentTime);
- //總時長
- totalTime=playerManager.getTotalTime();
- //遠端遷移恢復
- if (isInteractionPlay) {
- LogUtil.debug(TAG, "remotePlay,rewindTo:"+currentTime);
- playerManager.rewindTo(currentTime);
- if (!isPlaying) {
- return;
- }
- //播放
- playerManager.play();
- }
- }
問題總結(jié)
1.onMusicFinished 音樂播放完成時應該被調(diào)用,但是多數(shù)沒被調(diào)用,只是偶爾會調(diào)用,難道是我電腦性能跟不上了?
2.優(yōu)化了源碼中應用啟動后,點擊播放無法播放的問題
3.優(yōu)化了播放器播放完當前歌曲更新播放圖標
4.增加了相關(guān)的注釋說明
附件直接下載DistributedMusicPlayer.zip
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)