媒体会话-提供方
介绍
本示例主要展示了媒体会话(媒体提供方)的相关功能,使用@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内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!
鸿蒙【北向应用开发+南向系统层开发】文档
鸿蒙【基础+实战项目】视频
鸿蒙面经
为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!