鸿蒙媒体开发【媒体会话-提供方】音频和视频

媒体会话-提供方

介绍

本示例主要展示了媒体会话(媒体提供方)的相关功能,使用@ohos.multimedia.avsession等接口实现媒体提供方与媒体播控中心自定义信息的交互功能。

注意: 此示例仅展示媒体提供方的相关功能,如果需要媒体会话提供的完整的自定义信息交互功能,请将本示例与系统播控中心共同使用。

效果预览

1

使用说明
基础操作
  1. 打开媒体提供方示例应用。
  2. 点击播放按钮,应用的播放状态发生变化,进度开始刷新。
  3. 点击暂停按钮,应用的播放状态开始变化,进度停止刷新。
  4. 点击上一首按钮,应用界面展示播放列表中的上一首歌曲的信息。
  5. 点击下一首按钮,应用界面展示播放列表中的下一首歌曲的信息。
  6. 点击播放,拖动进度条,播放进度改变。
  7. 点击收藏,收藏按钮点亮。
  8. 点击播放模式,可以切换不同的播放模式(注:播放模式未实现功能,仅实现与播控中心状态同步)。
进阶操作(与媒体播控中心一起使用)
  1. 点击本应用播放、暂停、上一首、下一首按钮,可以发现媒体播控中心中,该会话的状态同步改变。
  2. 点击媒体播控中心的对应按钮,可以发现本应用中按钮状态同步改变。
  3. 媒体播控中心可以获取到本应用的正在播放内容、播放模式、歌词、进度、收藏状态、媒体资源金标、历史歌单等信息。

具体实现

  • 界面相关的实现都封装在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()来发送自定义数据包。

相关权限

  1. 长时任务权限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);
    }
    

依赖

此示例仅展示媒体提供方的相关功能,如果需要媒体会话提供的完整的自定义信息交互功能,请将本示例与媒体播控中心共同使用。

以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线,展示如下:
1

除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下

内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!

鸿蒙【北向应用开发+南向系统层开发】文档

鸿蒙【基础+实战项目】视频

鸿蒙面经

在这里插入图片描述

为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值