媒体会话-提供方
介绍
本示例主要展示了媒体会话(媒体提供方)的相关功能,使用@ohos.multimedia.avsession等接口实现媒体提供方与媒体播控中心自定义信息的交互功能。
注意: 此示例仅展示媒体提供方的相关功能,如果需要媒体会话提供的完整的自定义信息交互功能,请将本示例与系统播控中心共同使用。
效果预览
使用说明
基础操作
- 打开媒体提供方示例应用。
- 点击播放按钮,应用的播放状态发生变化,进度开始刷新。
- 点击暂停按钮,应用的播放状态开始变化,进度停止刷新。
- 点击上一首按钮,应用界面展示播放列表中的上一首歌曲的信息。
- 点击下一首按钮,应用界面展示播放列表中的下一首歌曲的信息。
- 点击播放,拖动进度条,播放进度改变。
- 点击收藏,收藏按钮点亮。
- 点击播放模式,可以切换不同的播放模式(注:播放模式未实现功能,仅实现与播控中心状态同步)。
进阶操作(与媒体播控中心一起使用)
- 点击本应用播放、暂停、上一首、下一首按钮,可以发现媒体播控中心中,该会话的状态同步改变。
- 点击媒体播控中心的对应按钮,可以发现本应用中按钮状态同步改变。
- 媒体播控中心可以获取到本应用的正在播放内容、播放模式、歌词、进度、收藏状态、媒体资源金标、历史歌单等信息。
具体实现
-
界面相关的实现都封装在pages/Index.ets下,源码参考:[pages/Index.ets]
/*
- Copyright (C) 2024 Huawei Device Co., Ltd.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
*/
import { avSession } from '@kit.AVSessionKit';
import MediaData from '../model/MediaData';
import Logger from '../common/utils/Logger';
import { ProviderFeature } from '../viewmodel/ProviderManager';
import Constants from '../common/constants/Constants';const TAG = 'Index';
@Entry
@Component
struct Index {
@StorageLink('SeekPosition') seekPosition: number = 0;
@StorageLink('CurrentLoopMode') currentLoopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE = 0;
@StorageLink('IsPlaying') isPlaying: boolean = false;
@StorageLink('isFavorMap') isFavorMap: Map<String, boolean> | null = null;
@StorageLink('CurrentPlayItem') currentPlayItem: avSession.AVQueueItem | null = null;
@StorageLink('CurrentAVMetadata') currentAVMetadata: avSession.AVMetadata | null = null;
@StorageLink('CurrentImage') currentImage: PixelMap | null = null;
@State isProgressSliding: Boolean = false;
private providerFeature: ProviderFeature | undefined = undefined;
private sliderTimer?: number;async aboutToAppear() { this.providerFeature = await ProviderFeature.getInstance(); } aboutToDisappear() { this.providerFeature!.unInit(); } onPageHide() { // The application page is returned to the background, and a long-time task keep-alive playback is applied for. this.providerFeature!.startContinuousTask(); } build() { Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { Row() { Image($r('app.media.ic_down')) .width($r('app.float.icon_width')) .height($r('app.float.icon_height')) } .width(Constants.PERCENT_FULL) .height($r('app.float.icon_area_height')) .margin({ top: $r('app.float.icon_area_marin_top') }) .justifyContent(FlexAlign.Start) .alignItems(VerticalAlign.Bottom) Image(this.currentImage ? this.currentImage : '') .width(Constants.PERCENT_FULL) .margin({ top: $r('app.float.image_margin_top') }) .id('CurrentImage') Row() { Text(this.currentAVMetadata!.title ? this.currentAVMetadata!.title : 'No title') .width(Constants.PERCENT_FULL) .fontSize($r('app.float.music_title_font_size')) .fontColor($r('app.color.music_title_font_color')) .fontWeight(FontWeight.Bold) .id('Title') } .height($r('app.float.title_area_height')) .alignItems(VerticalAlign.Bottom) Text(this.currentAVMetadata!.artist ? this.currentAVMetadata!.artist : 'No artist') .width(Constants.PERCENT_FULL) .height($r('app.float.artist_height')) .margin({ top: $r('app.float.artist_margin_top') }) .fontSize($r('app.float.artist_margin_font_size')) .fontColor($r('app.color.music_title_font_color')) .id('Artist') Blank() Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start, alignContent: FlexAlign.Center, justifyContent: FlexAlign.Center }) { Slider({ value: this.seekPosition, min: 0, max: Constants.ONE_HUNDRED, style: SliderStyle.OutSet }) .trackThickness($r('app.float.slider_track_thickness_normal')) .blockColor(Color.White) .trackColor($r('app.color.slider_track_color')) .selectedColor($r('app.color.slider_selected_color')) .showSteps(false) .showTips(false) .onChange((value: number, mode: SliderChangeMode) => { Logger.info(TAG, 'value:' + value + 'mode:' + mode.toString()) if (mode === SliderChangeMode.End) { this.providerFeature!.seek(value); } }) .onTouch((event: TouchEvent) => { Logger.info(TAG, 'progress touch: ' + event.type) if (event.type === TouchType.Up) { this.sliderTimer = setTimeout(() => { this.isProgressSliding = false; }, Constants.SLIDER_CHANGE_DURATION); } else { clearTimeout(this.sliderTimer); this.isProgressSliding = true; } }) } .height($r('app.float.slider_area_height')) Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, alignContent: FlexAlign.Center, justifyContent: FlexAlign.SpaceBetween }) { Button({ stateEffect: true }) { Image(this.isFavorMap!.get(this.currentAVMetadata!.assetId) ? $r('app.media.ic_public_favor_filled') : $r('app.media.ic_public_favor')) } .width($r('app.float.normal_button_width')) .aspectRatio(1) .backgroundColor(Color.Transparent) .id('Favorite') .onClick(async () => { await this.providerFeature!.toggleFavorite(); }) Button({ stateEffect: true }) { Image($r('app.media.ic_previous')) } .width($r('app.float.normal_button_width')) .aspectRatio(1) .backgroundColor(Color.Transparent) .id('Previous') .onClick(async () => { this.providerFeature!.previous(); }) Button({ stateEffect: true }) { Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play')) } .height($r('app.float.play_button_height')) .aspectRatio(1) .backgroundColor(Color.Transparent) .id('PlayOrPause') .onClick(async () => { if (!this.isPlaying) { await this.providerFeature!.play(); } else { await this.providerFeature!.pause(); } }) Button({ stateEffect: true }) { Image($r('app.media.ic_next')) } .width($r('app.float.normal_button_width')) .aspectRatio(1) .backgroundColor(Color.Transparent) .id('Next') .onClick(async () => { this.providerFeature!.next(); }) Button({ stateEffect: true }) { Image(MediaData.loopModeList[this.currentLoopMode!]) } .width($r('app.float.normal_button_width')) .aspectRatio(1) .backgroundColor(Color.Transparent) .id('LoopMode') .onClick(async () => { this.providerFeature!.loopMode(); }) } .width(Constants.PERCENT_FULL) .height($r('app.float.play_button_height')) .margin({ left: $r('app.float.button_row_margin_left'), right: $r('app.float.button_row_margin_left'), top: $r('app.float.button_row_margin_top'), bottom: $r('app.float.button_row_margin_bottom') }) } .height(Constants.PERCENT_FULL) .width(Constants.PERCENT_FULL) } .margin({ left: $r('app.float.page_margin_left'), right: $r('app.float.page_margin_left') }) } .width(Constants.PERCENT_FULL) .height(Constants.PERCENT_FULL) .backgroundImage($r('app.media.ic_background')) .backgroundImageSize(ImageSize.Cover) }
}
-
使用
@StorageLink
来设置与逻辑代码同步更新的变量,当逻辑代码中对应的变量更新时,界面会同步的刷新。-
通过引入逻辑代码对应的类,创建出对象,实现对onClick事件的响应,关键代码段:
import { ProviderFeature } from '../feature/ProviderFeature'; this.providerFeature = await ProviderFeature.getInstance(); // 创建对象单例 Button() { // 按钮的内容 } .onClick(async () => { this.providerFeature.play(); // 通过类的对象来调用逻辑代码 })
-
-
逻辑相关的实现都封装在viewmodel/ProviderManger.ets下,源码参考:[viewmodel/ProviderManager.ets]
/*
- Copyright (C) 2024 Huawei Device Co., Ltd.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
*/
import { wantAgent, common } from '@kit.AbilityKit';
import { util } from '@kit.ArkTS';
import { audio } from '@kit.AudioKit';
import { avSession } from '@kit.AVSessionKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
import { media } from '@kit.MediaKit';
import Constants, { AVPlayerState } from '../common/constants/Constants';
import GlobalContext from '../common/utils/GlobalContextUtils';
import MediaPlayerUtils from '../common/utils/MediaPlayerUtils';
import Logger from '../common/utils/Logger';
import MediaData from '../model/MediaData';/**
- Processes playback logic and interacts with the AVSession.
*/
const TAG = 'ProviderFeature';
export class ProviderFeature {
private static providerSelf: ProviderFeature | undefined = undefined;
private context: common.UIAbilityContext | undefined =
GlobalContext.getContext().getObject(Constants.CONTEXT) as common.UIAbilityContext;
private constants = new MediaData();
private session: avSession.AVSession | null = null;
private isPlayLink: SubscribedAbstractProperty<boolean> | null = null;
private currentPlayItemLink: SubscribedAbstractProperty<avSession.AVQueueItem> | undefined = undefined;
private currentLoopModeLink: SubscribedAbstractProperty<avSession.LoopMode> | undefined = undefined;
private seekPositionLink: SubscribedAbstractProperty<number> | undefined = undefined;
private currentAVMetadataLink: SubscribedAbstractProperty<avSession.AVMetadata> | undefined = undefined;
private currentImageLink: SubscribedAbstractProperty<PixelMap> | undefined = undefined;
private queueItems: Array<avSession.AVQueueItem> =
[this.constants.queueItemFirst, this.constants.queueItemSecond, this.constants.queueItemThird];
private queueItemPixelMapArray: Array<PixelMap> = [];
private MetadataPixelMapArray: Array<PixelMap> = [];
private resourceManager: resourceManager.ResourceManager = this.context!.resourceManager;
private avMetadataList: Array<avSession.AVMetadata> =
[this.constants.avMetadataFirst, this.constants.avMetadataSecond, this.constants.avMetadataThird];
private currentState: avSession.AVPlaybackState = {
state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE
};
private constantsForControl: MediaData = new MediaData();
private mediaPlayerUtil: MediaPlayerUtils = new MediaPlayerUtils();
private avPlayer?: media.AVPlayer;
private currentTime: number = 0;
private isFavorMapLink: SubscribedAbstractProperty<Map<String, boolean>> | undefined = undefined;
private playListAssetIdMap: Map<string, number> = new Map();
private localLyric: string = '';static async getInstance(): Promise<ProviderFeature> { Logger.info(TAG, ' provider getInstance'); if (!ProviderFeature.providerSelf) { Logger.info(TAG, ' new provider'); ProviderFeature.providerSelf = new ProviderFeature(); await ProviderFeature.providerSelf.init(); } return ProviderFeature.providerSelf; } private constructor() { this.isPlayLink = AppStorage.setAndLink('IsPlaying', false); this.currentPlayItemLink = AppStorage.setAndLink<avSession.AVQueueItem>('CurrentPlayItem', {} as avSession.AVQueueItem); this.currentAVMetadataLink = AppStorage.setAndLink<avSession.AVMetadata>('CurrentAVMetadata', {} as avSession.AVMetadata); this.isFavorMapLink = AppStorage.setAndLink<Map<String, boolean>>('isFavorMap', new Map()); this.currentImageLink = AppStorage.setAndLink<PixelMap>('CurrentImage', {} as PixelMap); this.currentLoopModeLink = AppStorage.setAndLink<avSession.LoopMode>('CurrentLoopMode', avSession.LoopMode.LOOP_MODE_SEQUENCE); this.seekPositionLink = AppStorage.setAndLink<number>('SeekPosition', 0); this.currentAVMetadataLink!.set(this.avMetadataList[0]); this.currentLoopModeLink!.set(avSession.LoopMode.LOOP_MODE_SEQUENCE); let map: Map<String, boolean> = new Map(); map.set(this.avMetadataList[0].assetId, false); map.set(this.avMetadataList[1].assetId, false); map.set(this.avMetadataList[2].assetId, false); this.isFavorMapLink!.set(map); this.playListAssetIdMap.set(Constants.ENTITY_ID_FIRST_PLAY_LIST, 0); this.playListAssetIdMap.set(Constants.ENTITY_ID_SECOND_PLAY_LIST, 1); this.playListAssetIdMap.set(Constants.ENTITY_ID_THIRD_PLAY_LIST, 2); } /** * Initialize resources. */ async init(): Promise<void> { this.avPlayer = await this.mediaPlayerUtil.init(); this.localLyric = await this.getLocalLrc(Constants.LRC_NAME); await this.prepareImageResources(); await this.prepareResourcesForController(); await this.CreateAVSession(); await this.InitFirstMusicState(); await this.RegisterAVPlayerListener(); } async unInit(): Promise<void> { this.UnRegisterListener(); if (this.avPlayer) { this.unRegisterAVPlayerListener(); this.avPlayer.release(); } } /** * Reads local lyrics. */ async getLocalLrc(fileName: string): Promise<string> { const value: Uint8Array = await this.context!.resourceManager.getRawFileContent(fileName); Logger.info(TAG, 'local lrc:' + fileName + ':' + value); if (!value) { Logger.error(TAG, 'get lyc fail:' + fileName); return Promise.reject('get lyc fail'); } let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true }); let retStr: string = textDecoder.decodeWithStream(value, { stream: false }); Logger.info(TAG, 'retStr: ' + retStr); return retStr; } /** * Start a long-time task. */ async startContinuousTask(): Promise<void> { let wantAgentInfo: wantAgent.WantAgentInfo = { wants: [ { bundleName: Constants.BUNDLE_NAME, abilityName: Constants.ABILITY_NAME } ], operationType: wantAgent.OperationType.START_ABILITY, requestCode: 0, wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] }; let tmpWantAgent = await wantAgent.getWantAgent(wantAgentInfo); // Sets the application page that can be invoked when a playback control card is clicked. this.session?.setLaunchAbility(tmpWantAgent); await backgroundTaskManager.startBackgroundRunning(this.context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, tmpWantAgent); } /** * Close a long-time task. */ async stopContinuousTask(): Promise<boolean> { Logger.info(TAG, 'stop background '); return new Promise((resolve) => { backgroundTaskManager.stopBackgroundRunning(this.context, (err, _) => { if (err) { Logger.error(TAG, `stop background startBackgroundRunning err ${err.stack}`); resolve(false); } else { Logger.info(TAG, `stop background startBackgroundRunning success`); resolve(true); } }); }); } /** * Create AVSession. */ async CreateAVSession(): Promise<boolean> { Logger.info(TAG, `Start create AVSession`); let ret: boolean = true; if (this.session) { Logger.info(TAG, `has a session: ` + this.session.sessionId); return ret; } // Create an audio session. The Favorites, Previous, Pause, and Repeat buttons are displayed on the playback control page. // A developer can also create a session of the video type. The playback control page displays the fast-forward, rewind, and pause buttons. this.session = await avSession.createAVSession(this.context!, 'AVSessionDemo', 'audio'); // Activate the session. this.RegisterSessionListener(); await this.session?.activate().catch((err: BusinessError) => { if (err) { Logger.error(TAG, `Failed to activate AVSession, error info: ${JSON.stringify(err)}`); ret = false; } }); return ret; } async InitFirstMusicState(): Promise<void> { Logger.info(TAG, ` InitFirstMusicState`); await this.mediaPlayerUtil.loadFromRawFile(Constants.MUSIC_FILE_NAME); this.isPlayLink!.set(false); this.currentImageLink!.set(this.MetadataPixelMapArray[0]); this.currentState.state = avSession.PlaybackState.PLAYBACK_STATE_PAUSE; await this.session?.setAVPlaybackState(this.currentState); await this.setAVMetadataToController(0); this.currentPlayItemLink!.set(this.queueItems![0]); this.currentAVMetadataLink!.set(this.avMetadataList[0]); } /** * Registers the session callback function. * Register the command as required. If the command is not supported, do not register it. * For the session of the audio type, implement the progress bar, favorites, next song, play/pause, and loop mode callback functions. If the functions are not supported, disable the interface or do not register the interface. * For a session of the video type, implement the fast-forward, rewind, next episode, and playback pause callback functions. If the functions are not supported, disable the interface or do not register the interface. */ async RegisterSessionListener(): Promise<void> { // Processes playback commands. this.session?.on('play', async () => { Logger.info(TAG, `on play, do play task`); this.avPlayer?.play(); this.isPlayLink!.set(true); this.currentState = { state: avSession.PlaybackState.PLAYBACK_STATE_PLAY, position: { elapsedTime: this.currentTime, updateTime: new Date().getTime(), } }; await this.session?.setAVPlaybackState(this.currentState); }); // Suspend instruction processing. this.session?.on('pause', async () => { Logger.info(TAG, `on pause, do pause task`); this.avPlayer?.pause(); this.isPlayLink!.set(false); this.currentState = { state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: this.currentTime, updateTime: new Date().getTime(), } }; await this.session?.setAVPlaybackState(this.currentState); }); // Stop instruction processing. this.session?.on('stop', async () => { Logger.info(TAG, `on stop , do stop task`); this.avPlayer?.stop(); this.isPlayLink!.set(false); this.currentState.state = avSession.PlaybackState.PLAYBACK_STATE_PAUSE; await this.session?.setAVPlaybackState(this.currentState); }); // Next song/set instruction processing. this.session?.on('playNext', async () => { Logger.info(TAG, `on playNext , do playNext task`); let nextId: number = this.currentPlayItemLink!.get().itemId + 1; nextId = this.queueItems!.length > nextId ? nextId : nextId - this.queueItems!.length; await this.handleNewItem(nextId); }); // Previous song/set instruction processing. this.session?.on('playPrevious', async () => { Logger.info(TAG, `on playPrevious , do playPrevious task`); let previousId: number = this.currentPlayItemLink!.get().itemId - 1; previousId = previousId < 0 ? previousId + this.queueItems!.length : previousId; await this.handleNewItem(previousId); }); // Processes the progress bar dragging command. this.session?.on('seek', (position) => { Logger.info(TAG, 'on seek: seek to' + position); // Modify the playback progress based on the instruction. if (position >= this.currentAVMetadataLink!.get().duration!) { this.next(); } else { this.avPlayer?.seek(position); this.currentState.position = { elapsedTime: position, updateTime: new Date().getTime() }; this.session?.setAVPlaybackState(this.currentState); } }); // Processes the favorite/like command for the audio session. this.session?.on('toggleFavorite', (assetId) => { // If a system callback message is received, the user clicks the favorites button when playing the song. // The app stores the favorites status based on the song ID and reports the current favorites status. Logger.info(TAG, 'on toggleFavorite session, do toggleFavorite task: ' + assetId); this.isFavorMapLink!.get().set(assetId, !this.isFavorMapLink!.get().get(assetId)); this.currentState.isFavorite = this.isFavorMapLink!.get().get(assetId); this.session?.setAVPlaybackState(this.currentState); }); // Cyclic mode instruction processing for audio session. this.session?.on('setLoopMode', (mode) => { Logger.info(TAG, 'on setLoopMode: ' + mode); // The value transferred by the playback control is not processed. // The value is switched based on the application sequence. let currentMode = this.currentLoopModeLink!.get(); this.currentLoopModeLink!.set(currentMode === 3 ? 0 : currentMode + 1); // The playback status is updated. The cyclic mode after application processing is reported in the playback status. this.currentState.loopMode = this.currentLoopModeLink!.get(); Logger.info(TAG, 'self setLoopMode: ' + this.currentState.loopMode); this.session?.setAVPlaybackState(this.currentState); }); // Fast-forward command processing for video sessions. this.session?.on('fastForward', (skipInterval?: number) => { let currentTime: number = (skipInterval! * 1000 + this.avPlayer!.currentTime) > this.currentAVMetadataLink!.get().duration! ? this.currentAVMetadataLink!.get().duration! : (skipInterval! * 1000 + this.avPlayer!.currentTime); if (currentTime >= this.currentAVMetadataLink!.get().duration!) { this.next(); } else { this.avPlayer?.seek(currentTime); this.currentState.position = { elapsedTime: currentTime, updateTime: new Date().getTime() }; this.currentState.state = avSession.PlaybackState.PLAYBACK_STATE_PLAY; this.session?.setAVPlaybackState(this.currentState); } }); // Rewind command processing, for video session. this.session?.on('rewind', (skipInterval?: number) => { let currentTime: number = (this.avPlayer!.currentTime - skipInterval! * 1000) <= 0 ? 0 : (this.avPlayer!.currentTime - skipInterval! * 1000); this.avPlayer?.seek(skipInterval); Logger.info(TAG, ' currentTime' + JSON.stringify(currentTime)); if (currentTime <= 0) { this.previous(); } else { this.avPlayer?.seek(currentTime); this.currentState.position = { elapsedTime: currentTime, updateTime: new Date().getTime() }; this.currentState.state = avSession.PlaybackState.PLAYBACK_STATE_PLAY; this.session?.setAVPlaybackState(this.currentState); } }); } /** * Deregister a session callback. */ async UnRegisterListener(): Promise<void> { if (this.session) { this.session.off('play'); this.session.off('pause'); this.session.off('stop'); this.session.off('playNext'); this.session.off('playPrevious'); this.session.off('fastForward'); this.session.off('rewind'); this.session.off('seek'); this.session.off('setLoopMode'); this.session.off('toggleFavorite'); // Destroys a created session. this.session.destroy((err) => { if (err) { Logger.info(TAG, `Destroy BusinessError: code: ${err.code}, message: ${err.message}`); } else { Logger.info(TAG, 'Destroy : SUCCESS'); } }); } } /** * Processing logic of the avplayer when the playback is paused. */ async localPlayOrPause(): Promise<void> { Logger.info(TAG, 'localPlayOrPause start play' + this.avPlayer?.state); if (!this.avPlayer) { Logger.error(TAG, 'localPlayOrPause start play no avplayer'); return; } if (this.avPlayer.state === AVPlayerState.PLAYING) { Logger.info(TAG, 'localPlayOrPause start play start pause'); await this.avPlayer.pause(); this.isPlayLink!.set(false); } else if (this.avPlayer.state === AVPlayerState.STOPPED) { Logger.info(TAG, 'localPlayOrPause start play from stopped'); await this.avPlayer.prepare(); await this.avPlayer.play(); this.isPlayLink!.set(true); } else { Logger.info(TAG, 'localPlayOrPause start play'); await this.avPlayer.play(); this.isPlayLink!.set(true); Logger.info(TAG, 'localPlayOrPause start play done'); } Logger.info(TAG, 'localPlayOrPause isPlay: ' + this.isPlayLink!.get()); } /** * Set AVMetadata. */ async setAVMetadataToController(itemId: number): Promise<void> { let avMetadata: avSession.AVMetadata = this.constantsForControl.avMetadataList[itemId]; avMetadata.lyric = this.localLyric; Logger.info(TAG, `setAVMetadataToController avMetadata: ` + JSON.stringify(avMetadata)); await this.session?.setAVMetadata(avMetadata); } /** * In-app favorites button. */ async toggleFavorite(): Promise<void> { this.isFavorMapLink!.get() .set(this.currentAVMetadataLink!.get().assetId, !this.isFavorMapLink!.get().get(this.currentAVMetadataLink!.get().assetId)); this.currentState.isFavorite = this.isFavorMapLink!.get().get(this.currentAVMetadataLink!.get().assetId); Logger.info(TAG, ` Start do toggleFavorite task state isFavorite: ` + this.currentState.isFavorite); this.session?.setAVPlaybackState(this.currentState); } /** * In-app circulation mode. */ async loopMode(): Promise<void> { let currentMode = this.currentLoopModeLink!.get(); Logger.info(TAG, 'do setLooMode old: ' + currentMode); this.currentLoopModeLink!.set(currentMode === Constants.MAX_LOOP_MODE_COUNT ? 0 : ++currentMode); this.currentState.loopMode = this.currentLoopModeLink!.get(); Logger.info(TAG, 'do setLooMode new: ' + this.currentState.loopMode); this.session?.setAVPlaybackState(this.currentState); } /** * Updates the playback status. */ async play(): Promise<void> { Logger.info(TAG, `Start do play task`); await this.localPlayOrPause(); let isFavor: boolean | undefined = this.isFavorMapLink!.get().get(this.currentAVMetadataLink!.get().assetId); Logger.info(TAG, `currentState assetId ` + this.currentAVMetadataLink!.get().assetId); Logger.info(TAG, `currentState isPlay ` + this.isPlayLink!.get()); this.currentState = { state: this.isPlayLink!.get() ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: this.currentTime, updateTime: new Date().getTime() }, isFavorite: isFavor }; Logger.info(TAG, `currentState` + JSON.stringify(this.currentState)); await this.session?.setAVPlaybackState(this.currentState); } /** * In-app pause button. */ async pause(): Promise<void> { Logger.info(TAG, `on pause , do pause task`); await this.localPlayOrPause(); let isFavor: boolean | undefined = this.isFavorMapLink!.get().get(this.currentAVMetadataLink!.get().assetId); Logger.info(TAG, `currentState assetId ` + this.currentAVMetadataLink!.get().assetId); Logger.info(TAG, `currentState isPlay ` + this.isPlayLink!.get()); this.currentState = { state: this.isPlayLink!.get() ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: this.currentTime, updateTime: new Date().getTime() }, isFavorite: isFavor }; Logger.info(TAG, `currentState` + JSON.stringify(this.currentState)); await this.session?.setAVPlaybackState(this.currentState); } /** * In-app previous song button. */ async previous(): Promise<void> { Logger.info(TAG, `on playPrevious , do playPrevious task`); let previousId: number = this.currentPlayItemLink!.get().itemId - 1; previousId = previousId < 0 ? previousId + this.queueItems!.length : previousId; await this.handleNewItem(previousId); } /** * In-app next song button. */ async next(): Promise<void> { Logger.info(TAG, `on playNext , do playNext task`); let nextId: number = this.currentPlayItemLink!.get().itemId + 1; nextId = this.queueItems!.length > nextId ? nextId : nextId - this.queueItems!.length; await this.handleNewItem(nextId); } /** * In-app progress bar. */ async seek(value: number): Promise<void> { Logger.info(TAG, `on seek , do seek task to: ` + value); let currentPosition = value / 100 * this.currentAVMetadataLink!.get().duration!; if (currentPosition >= this.currentAVMetadataLink!.get().duration!) { this.next(); } else { this.avPlayer?.seek(currentPosition); this.currentState.position = { elapsedTime: Math.floor(currentPosition), updateTime: new Date().getTime() }; this.session?.setAVPlaybackState(this.currentState); } } /** * Processes the previous/next command. */ async handleNewItem(itemId: number): Promise<void> { Logger.info(TAG, ' handleNewItem itemId: ' + itemId); this.currentImageLink!.set(this.MetadataPixelMapArray[itemId]); await this.setAVMetadataToController(itemId); this.currentPlayItemLink!.set(this.queueItems![itemId]); this.currentAVMetadataLink!.set(this.avMetadataList[this.currentPlayItemLink!.get().itemId]); await this.mediaPlayerUtil.loadFromRawFile(Constants.MUSIC_FILE_NAME); // The avplayer is ready to play. this.mediaPlayerUtil.on('prepared', () => { Logger.info(TAG, 'AVPlayer state prepared, start play'); this.handleNewItemAVPlayback(); }); } async handleNewItemAVPlayback(): Promise<void> { await this.localPlayOrPause(); let isFavor: boolean | undefined = this.isFavorMapLink!.get().get(this.currentAVMetadataLink!.get().assetId); Logger.info(`currentState assetId ` + this.currentAVMetadataLink!.get().assetId); Logger.info(`currentState isFavor ` + isFavor); this.currentState = { state: this.isPlayLink!.get() ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: this.currentTime, updateTime: new Date().getTime() }, isFavorite: isFavor }; Logger.info(`currentState` + JSON.stringify(this.currentState)); await this.session?.setAVPlaybackState(this.currentState); } /** * Processes the playback of historical playlists. */ async handleNewPlayListItem(avQueueId: string): Promise<void> { let assetId: number = this.playListAssetIdMap.get(avQueueId)!; await this.handleNewItem(assetId); } /** * Prepare media resources. */ async prepareImageResources(): Promise<void> { Logger.info(TAG, `prepareImageResources in`); this.queueItemPixelMapArray.push(await this.saveRawFileToPixelMap('first.png')); this.queueItemPixelMapArray.push(await this.saveRawFileToPixelMap('second.png')); this.queueItemPixelMapArray.push(await this.saveRawFileToPixelMap('third.png')); this.MetadataPixelMapArray.push(await this.saveRawFileToPixelMap('first_with_background.png')); this.MetadataPixelMapArray.push(await this.saveRawFileToPixelMap('second_with_background.png')); this.MetadataPixelMapArray.push(await this.saveRawFileToPixelMap('third_with_background.png')); for (let i = 0; i < this.queueItemPixelMapArray.length; i++) { this.queueItems[i].description!.mediaImage = this.queueItemPixelMapArray[i]; this.avMetadataList[i].mediaImage = this.MetadataPixelMapArray[i]; this.avMetadataList[i].avQueueImage = this.queueItemPixelMapArray[i]; } this.currentPlayItemLink!.set(this.queueItems![0]); this.currentImageLink!.set(this.MetadataPixelMapArray[0]); this.currentAVMetadataLink!.set(this.avMetadataList[0]); } async saveRawFileToPixelMap(rawFilePath: string): Promise<image.PixelMap> { let value: Uint8Array = await this.resourceManager.getRawFileContent(rawFilePath); let imageBuffer: ArrayBuffer = value.buffer as ArrayBuffer; let imageSource: image.ImageSource = image.createImageSource(imageBuffer); let imagePixel: image.PixelMap = await imageSource.createPixelMap({ desiredSize: { width: Constants.IMAGE_PIXEL_MAP_WIDTH, height: Constants.IMAGE_PIXEL_MAP_WIDTH } }); return imagePixel; } async prepareResourcesForController(): Promise<void> { Logger.info(TAG, `prepareResourcesForController in`); this.constantsForControl.avMetadataList[0].mediaImage = await this.saveRawFileToPixelMap('first.png'); this.constantsForControl.avMetadataList[0].avQueueImage = await this.saveRawFileToPixelMap('first.png'); this.constantsForControl.avMetadataList[1].mediaImage = await this.saveRawFileToPixelMap('second.png'); this.constantsForControl.avMetadataList[1].avQueueImage = await this.saveRawFileToPixelMap('second.png'); this.constantsForControl.avMetadataList[2].mediaImage = await this.saveRawFileToPixelMap('third.png'); this.constantsForControl.avMetadataList[2].avQueueImage = await this.saveRawFileToPixelMap('third.png'); } /** * Registers the avplayer event listener. */ async RegisterAVPlayerListener(): Promise<void> { // Registers focus interrupt listening. Logger.info(TAG, ` RegisterAVPlayerListener`); this.avPlayer?.on('audioInterrupt', (info: audio.InterruptEvent) => { Logger.info(TAG, 'audioInterrupt success,and InterruptEvent info is:' + info); if (this.avPlayer?.state === AVPlayerState.PLAYING) { Logger.info(TAG, 'audio interrupt, start pause'); this.avPlayer?.pause(); this.currentState.state = avSession.PlaybackState.PLAYBACK_STATE_PAUSE; this.session?.setAVPlaybackState(this.currentState); } }); // Registers the playback time change callback function. this.avPlayer?.on('timeUpdate', (time: number) => { // The function of obtaining the current location globally is added. this.currentTime = time; Logger.info(TAG, 'time update progress:' + time); this.seekPositionLink!.set(time / this.currentAVMetadataLink!.get().duration! * 100); }); } /** * UnRegister the event listener for the avplayer. */ async unRegisterAVPlayerListener(): Promise<void> { Logger.info(TAG, ` unRegisterAVPlayerListener`); // UnRegister the listening of focus interrupt. this.avPlayer?.off('audioInterrupt'); // UnRegister the playback time change callback function. this.avPlayer?.off('timeUpdate'); this.avPlayer?.off('stateChange'); this.avPlayer?.off('seekDone'); this.avPlayer?.off('error'); }
}
-
应用的初始化相关操作
-
链接变量
通过
AppStorage.SetAndLink()
将逻辑代码中的变量与界面代码中使用@StorageLink
声明的变量连接起来,通过set()
与get()
操作来修改或获取变量的值,关键代码段:private isPlayLink: SubscribedAbstractProperty<boolean> = null; this.isPlayLink = AppStorage.SetAndLink('IsPlaying', false); this.isPlayLink.set(false); // 设置变量的值 let currentState : boolean = this.isPlayLink.get(); // 获取变量的值
-
创建并设置媒体会话
通过接口
createAVSession()
创建媒体会话;通过接口
activate()
激活媒体会话;通过接口
setAVMetadata()
设置当前媒体的元数据,设置后媒体播控中心可以读取使用此信息;通过接口
setAVPlaybackState()
设置当前媒体的播放信息,包括播放状态、播放进度,设置后媒体播控中心可以读取使用此信息;通过接口
on()
开启对媒体播控中心控制命令的监听,对媒体播控中心的命令进行处理,请激活媒体会话后再调用;
应用在运行中相关的操作
-
切换歌曲
在切换歌曲时,除了需要设置媒体提供方自身的状态,还需要使用接口
setAVPlaybackState()
与接口setAVMetadata()
将当前播放状态与元数据同步给媒体播控中心。 -
发送自定义数据包
媒体提供方可以使用接口
dispatchSessionEvent()
与接口setExtras()
来发送自定义数据包。
-
相关权限
-
长时任务权限ohos.permission.KEEP_BACKGROUND_RUNNING
如果需要让媒体提供方应用在后台运行或响应命令,需要注册长时任务权限[ohos.permission.KEEP_BACKGROUND_RUNNING]
请在需要后台运行的Ability的
module.json5
中添加以下配置:{ "module": { "requestPermissions": [ { "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" } ] } }
添加配置后,需要在逻辑代码中进行申请长时任务的操作,示例代码如下:
async startContinuousTask(){ let wantAgentInfo = { wants:[ { bundleName: "com.samples.mediaprovider", abilityName:"EntryAbility" } ], operationType : WantAgent.OperationType.START_ABILITY, requestCode: 0, wantAgentFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] }; let want = await WantAgent.getWantAgent(wantAgentInfo); await backgroundTaskManager.startBackgroundRunning(globalThis.context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,want); }
依赖
此示例仅展示媒体提供方的相关功能,如果需要媒体会话提供的完整的自定义信息交互功能,请将本示例与媒体播控中心共同使用。
以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线 ,展示如下:
除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下:
内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!
鸿蒙【北向应用开发+南向系统层开发】文档
鸿蒙【基础+实战项目】视频
鸿蒙面经
为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!