鸿蒙(API 12 Beta3版)【使用投播组件】案例应用

华为视频接入播控中心和投播能力概述**

华为视频在进入影片详情页播放时,支持在控制中心查看当前播放的视频信息,并进行快进、快退、拖动进度、播放暂停、下一集、调节音量等操作,方便用户通过控制中心来操作当前播放的视频。

当用户希望通过大屏播放当前华为视频的影片时,可以在华为视频或播控中心内进行投播,将影片投播到同一网络下的华为智慧屏等大屏设备进行播放,且通过播控中心来方便地进行播放暂停、快进快退、下一集等操作。

华为视频投播功能需要使用播控中心的能力完成,所以在接入投屏之前,华为视频需要先接入播控中心。

华为视频接入播控中心

华为视频接入播控中心介绍

  • 媒体会话(AVSession):本地播放时用于更新媒体资源的信息和响应系统播控中心,接入参考[媒体会话提供方]。在投播时,AVSession作为在本地播放和投播之间切换的“枢纽”接口,把二者联系起来。通过AVSession可以设置和查询应用投播能力,并创建投播控制器。
  • 媒体会话控制器(AVSessionController):一般由播控中心提供。如果是应用内的控制器,可用于控制应用的后台播放。

华为视频接入播控中心的交互流程如图所示。

1

华为视频同步播控中心

  • 播放内容信息上报播控中心

    这部分功能负责实现在应用播放的时候,通知播控中心当前播放的影片信息。

    • 应用冷启动之后,需要调用[createAVSession]创建会话。应用生命周期结束后会话自动销毁,不需要调用destroy。

说明

下文中代码示例,可能包含重复的函数和导包引入,因此后续代码示例不再重复展示。

  1. 导入相关模块
// MainAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AvSessionManager } from '../avsession/AvSessionManager';
import router from '@system.router';
// AvSessionManager.ts
import avSession from '@ohos.multimedia.avsession';
import hilog from '@ohos.hilog';
import type { BusinessError } from '@ohos.base';
import type common from '@ohos.app.ability.common';
import WantAgent from '@ohos.app.ability.wantAgent';
// 业务Index.ets
import avSession from '@ohos.multimedia.avsession';
import { AvSessionManager } from '../avsession/AvSessionManager';
  1. 调用[createAVSession]创建会话相关示例代码如下:
// MainAbility.ets
export default class MainAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AvSessionManager.getInstance().init(this.context);
  }
}
// AvSessionManager.ts
const TAG = 'AvSessionManager';

/**
 * 对接播控中心管理器
 */
export class AvSessionManager {

  private static readonly instance: AvSessionManager = new AvSessionManager();
  private session: avSession.AVSession = null;

  static getInstance(): AvSessionManager {
    return this.instance;
  }

  init(abilityContext: common.Context): void {
    avSession.createAVSession(abilityContext, 'himovie', 'video').then(session => {
      this.session = session;
      // 创建完成之后,激活会话。
      this.session.activate();
      hilog.info(0x06666, TAG, 'createAVSession success');
    }).catch((error: BusinessError) => {
      hilog.error(0x06666, TAG, `createAVSession or activate failed, code: ${error?.code}`);
    });
  }
}
  • 根据当前播放的Volume信息,拼接填写[setAVMetadata]。
// 业务Index.ets
@Entry
@Component
struct Index {
  private avsessionMetaData: avSession.AVMetadata | null = null;

  aboutToAppear(): void {
    this.setAVSessionMetaData();
  }

  setAVSessionMetaData() {
    this.avsessionMetaData = {
      // 影片的id
      assetId: 'test vod id',
      subtitle: 'vod subtitle',
      artist: 'artist name',
      title: 'vod title',
      mediaImage: 'media image url',
      // 仅支持投屏到Cast+ Stream的设备
      filter: avSession.ProtocolType.TYPE_CAST_PLUS_STREAM,
      // 快进快退时间
      skipIntervals: avSession?.SkipIntervals?.SECONDS_30
    };
    AvSessionManager.getInstance().setMetaData(this.avsessionMetaData);
  }
  build() {
    // ...
  }
}
// AvSessionManager.ts
export class AvSessionManager {
  private static readonly instance: AvSessionManager = new AvSessionManager();
  private session: avSession.AVSession = null;

  static getInstance(): AvSessionManager {
    return this.instance;
  }

  /**
   * 设置metaData并初始化状态
   *
   * @param metadata 影片元数据
   */
  setMetaData(metadata: avSession.AVMetadata): void {
    if (this.session) {

      hilog.info(0x06666, TAG, `setMetaData avMetadata: ${JSON.stringify(metadata)}`);

      this.session?.setAVMetadata(metadata)
        .then(() => {
          hilog.info(0x06666, TAG, `setMetaData success.`);
        })
        .catch((error: BusinessError) => {
          hilog.error(0x06666, TAG, `setMetaData failed, code: ${error.code}`);
        });
    }
  }
}
  • 播放状态上报播控中心

参考以下示例代码,向播控中心上报应用当前的播放状态。即应用中进行播放、暂停、进度调整等行为,通知播控中心进行不同的状态显示。

// AvSessionManager.ts
export class AvSessionManager {

  private static readonly instance: AvSessionManager = new AvSessionManager();
  private session: avSession.AVSession = null;
  /** 播放状态 */
  playState?: avSession.AVPlaybackState = {
    state: avSession.PlaybackState.PLAYBACK_STATE_INITIAL,
    position: {
      elapsedTime: 0,
      updateTime: (new Date()).getTime()
    }
  };

  static getInstance(): AvSessionManager {
    return this.instance;
  }

  /**
   * 播放
   *
   * @returns
   */
  play(currentTime?: number): void {
    hilog.info(0x0666, TAG, `AVSession play, currentTime:${currentTime}, state: ${this.playState?.state}`);
    this.setPlayOrPauseToAvSession('play', currentTime);
  }

  /**
   * 暂停
   *
   * @returns
   */
  pause(currentTime?: number): void {
    hilog.info(0x0666, TAG, `AVSession pause, currentTime: ${currentTime}, state: ${this.playState?.state}`);
    this.setPlayOrPauseToAvSession('pause', currentTime);
  }



  /**
   * 设置播控中心的状态为播放或暂停
   *
   * @param state 状态
   * @param elapsedTime 当前进度
   */
  private setPlayOrPauseToAvSession(state: 'play' | 'pause', elapsedTime: number): void {
    if (elapsedTime === undefined || elapsedTime < 0) {
      hilog.warn(0x0666, TAG, `param error, elapsedTime: ${elapsedTime}, do not play or pause.`);
      return;
    }
    if (this.playState === undefined || this.playState.state === avSession.PlaybackState.PLAYBACK_STATE_STOP) {
      hilog.warn(0x0666, TAG, `playState error, state is PLAYBACK_STATE_STOP or undefined, do not play or pause.`);
      return;
    }

    this.playState.state = state === 'play' ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE;
    this.playState.position = {
      elapsedTime: elapsedTime,
      updateTime: (new Date()).getTime()
    };
    this.setAVPlaybackState();
  }

  /**
   * 向播控中心设置播放状态
   */
  private setAVPlaybackState(): void {
    hilog.info(0x0666, TAG, `setAVPlaybackState state: ${this.playState.state}, updateTime: ${this.playState?.position?.updateTime}, speed: ${this.playState?.speed}`);
    this.session?.setAVPlaybackState(this.playState);
  }
}
  • 详情页退出的特殊逻辑

当用户从详情页退出到应用首页时,需要通知AVSession清除播放信息。

// AvSessionManager.ts
export class AvSessionManager {

  private static readonly instance: AvSessionManager = new AvSessionManager();
  private session: avSession.AVSession = null;
  /** 播放状态 */
  playState?: avSession.AVPlaybackState = {
    state: avSession.PlaybackState.PLAYBACK_STATE_INITIAL,
    position: {
      elapsedTime: 0,
      updateTime: (new Date()).getTime()
    }
  };

  /**
   * 向播控中心设置播放状态
   */
  private setAVPlaybackState(): void {
    hilog.info(0x0666, TAG, `setAVPlaybackState state: ${this.playState.state}, updateTime: ${this.playState?.position?.updateTime}, speed: ${this.playState?.speed}`);
    this.session?.setAVPlaybackState(this.playState);
  }

  /**
   * 释放播放器
   */
  releasePlayer(): void  {
    this.playState.state = avSession.PlaybackState.PLAYBACK_STATE_STOP;
    this.setAVPlaybackState();
  }
}

华为视频响应播控中心

当应用处于正常播放的状态时,播放信息和状态同步到播控中心,用户可以在播控中心控制媒体,如暂停、进度调整等。用户在播控中心操作后,需要应用配合响应各种事件,通过AVSession的各种回调完成播放控制。

应用如果已切换到后台,用户点击播控中心,将由播控中心负责[拉起华为视频]。应用需要配置拉起参数。

同时,应用需要[设置监听回调],包括播放、暂停、下一首、进度调整等。只有设置了回调,播控中心侧的按钮才会亮起来,否则按钮将会置灰。

  • 拉起华为视频
// AvSessionManager.ts
export class AvSessionManager {
  private static readonly instance: AvSessionManager = new AvSessionManager();
  private session: avSession.AVSession = null;
  /** 播放状态 */
  playState?: avSession.AVPlaybackState = {
    state: avSession.PlaybackState.PLAYBACK_STATE_INITIAL,
    position: {
      elapsedTime: 0,
      updateTime: (new Date()).getTime()
    }
  };

  static getInstance(): AvSessionManager {
    return this.instance;
  }

  /**
   * 设置metaData并初始化状态
   *
   * @param metadata 影片元数据
   */
  setMetaData(metadata: avSession.AVMetadata): void {
    if (this.session) {
      hilog.info(0x06666, TAG, `setMetaData avMetadata: ${JSON.stringify(metadata)}`);

      this.session?.setAVMetadata(metadata)
        .then(() => {
          hilog.info(0x06666, TAG, `setMetaData success.`);
          this.setLaunchAbility(metadata.assetId);
        })
        .catch((error: BusinessError) => {
          hilog.error(0x06666, TAG, `setMetaData failed, code: ${error.code}`);
        });
    }
  }

  /**
   * 设置一个WantAgent用于拉起会话的Ability
   * @param vodId 影片Id
   */
  setLaunchAbility(vodId: string): void {
    const ability: WantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'com.huawei.hmsapp.himovie',
          abilityName: 'MainAbility',
          parameters: {
            type: 'avsession',
            routeParams: {
              vodId,
            }
          }
        }
      ],
      requestCode: 0,
      actionType: WantAgent.OperationType.START_ABILITY,
      actionFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    }
    this.session.setLaunchAbility(ability).then(() => {
      hilog.info(0x0666, TAG, `SetLaunchAbility successfully`);
    }).catch((err: BusinessError) => {
      hilog.info(0x0666, TAG, `SetLaunchAbility failed, code: ${err.code}`);
    });
  }
}
  • 设置监听回调
// AvSessionManager.ts
export class AvSessionManager {
  private session: avSession.AVSession = null;

  /**
   * 监听播控中心回调事件,播放
   *
   * @param action 回调方法
   */
  onPlay(action: () => void): void {
    if (this.session) {
      this.session.on('play', action);
    }
  }

  /**
   * 监听播控中心回调事件,暂停
   *
   * @param action 回调方法
   */
  onPause(action: () => void): void {
    if (this.session) {
      this.session.on('pause', action);
    }
  }
}

华为视频支持投播

华为视频应用内发起投播

用户使用华为视频播放影片时,通过点击右上角投播组件1
,选择需要投播的大屏设备,连接成功后即可完成投播的流程。效果如下图所示。

图1 从华为视频内播放到投播成功
1

实现投播效果需要完成以下步骤。

  • 使用隔空投放组件连接远端设备

用户在播放影片时,右上角会展示一个1
图标,它提供了[投播]能力。用户点击该图标后,播控中心将拉起设备选择的模态窗口,设备的搜索发现、用户选择设备后的连接均由播控中心完成,此过程华为视频不感知。完成连接后,播控中心通过播放设备变化的监听事件outputDeviceChange通知华为视频,华为视频再进行下一步处理。

图2 点击投播组件触发设备选择弹框
1

应用使用[AVSession.on(‘outputDeviceChange’)]设置播放设备变化的监听事件,示例代码如下。

远端设备能够投播,需要满足以下条件:

  • 设备连接成功,即outputDeviceChange事件监听回调返回connectState为1。

  • OutputDeviceInfo中设备列表的第一个设备,必须为远端设备,即castCategory为CATEGORY_REMOTE。

  • 投播协议类型必须支持Cast+ Stream。

  1. 导入相关模块
// CastType.ts
import media from '@ohos.multimedia.media';
// 业务Index.ets
import avSession from '@ohos.multimedia.avsession';
import { AvSessionManager } from '../avsession/AvSessionManager';
import { CastManager } from '../avsession/CastManager';
import hilog from '@ohos.hilog';
import type { BusinessError } from '@ohos.base';
// CastManager.ets
import avSession from '@ohos.multimedia.avsession';
import hilog from '@ohos.hilog';
import type { BusinessError } from '@ohos.base';
import { CastMediaInfo, M3U8Info, CastMediaInfoType, CastErrorType } from './CastType';
import wantAgent from '@ohos.app.ability.wantAgent';
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import promptAction from '@ohos.promptAction';
  1. 设置播放设备变化的监听事件示例代码:
// CastManager.ets
const TAG = 'CastManager';

/**
 * 投播管理器
 */
export class CastManager {
  /** 单例 */
  private static readonly INSTANCE: CastManager = new CastManager();
  /** 播控中心avSession */
  private avSession?: avSession.AVSession;
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;

  public afterCreateSession(session: avSession.AVSession) {
    this.avSession = session;
    // 监听设备连接状态的变化
    this.setOutputDeviceChangeListener();
  }

  /**
   * 设置输出设备变化监听器
   */
  private setOutputDeviceChangeListener(): void {
    this.avSession?.on('outputDeviceChange', (connectState: avSession.ConnectionState,
      device: avSession.OutputDeviceInfo) => {
      const castCategory = device?.devices?.[0].castCategory;
      // 成功连接远程设备
      if (castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_CONNECTED) {
        // 获取cast控制器
        this.avSession?.getAVCastController().then(async (controller: avSession.AVCastController) => {
          hilog.info(0x0666, TAG, 'success to get avController');
          this.avCastController = controller;
          this.startCast();
        })
      }

      // 远端断开 或 本地连上 都算断开投播
      const isDisConnect = (castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_DISCONNECTED)
        || (castCategory === avSession.AVCastCategory.CATEGORY_LOCAL && connectState === avSession.ConnectionState.STATE_CONNECTED);
      if (isDisConnect) {
        this.stopCast();
      }
    });
  }

  /**
   * 开始投播
   */
  private startCast(): void {
    // ...
  }

  /**
   * 结束投播
   */
  public stopCast(): void {
    // 通知avSession结束投播
    this.avSession?.stopCasting();
  }
}
  • 获取投播的视频信息

华为视频目前获取投播的URL,是通过单独查询playVod播放鉴权接口,此接口返回一个HLS的多码率播放地址。

为了支持在华为视频内部切换清晰度的需求,需要对这个HLS的多码率播放地址进行解析,获取到多个清晰度的二级索引地址。

图3 应用播放框内投播选择清晰度
1

// CastType.ts
// CastType文件用于存放一些公共的类型定义

/**
 * 媒体信息的类型:在线视频、本地视频
 */
export type CastMediaInfoType = 'online' | 'local';

/**
 * 媒体信息
 */
export class CastMediaInfo {
  /**
   * 媒体信息的类型
   * online:在线视频投播
   * local:本地视频投播
   */
  type: CastMediaInfoType;
  /**
   * vodId
   */
  vodId?: string;
  /**
   * 剧集id
   */
  volumeId?: string;
  /**
   * url
   */
  url: string;
  /**
   * 清晰度
   */
  clarity?: string;
  /**
   * 文件句柄
   */
  fdSrc?: media.AVFileDescriptor;
  /**
   * 展示错误类型
   */
  playErrType?: number;
  /**
   * 展示错误码
   */
  playErrCode?: number;
}

/**
 * 解析m3u8的信息
 */
export class M3U8Info {
  /**
   * 播放地址
   */
  playUrl?: string;
  /**
   * 带宽
   */
  bandwidth: number = Number.NaN;
  /**
   * 分辨率:0x0
   */
  resolution?: string;
  /**
   * 媒体分辨率:例如720、1080等,取高度
   */
  mediaResolution: number = Number.NaN;
  /**
   * 清晰度
   */
  clarity: string = '';
}

/**
 * 给页面返回的错误类型
 */
export type CastErrorType = 'avSessionError' | 'playVodError';
// CastManager.ets
export class CastManager {
  /** 获取媒体uri */
  private getMediaInfoFunction: () => CastMediaInfo = () => new CastMediaInfo();
  /** 媒体资源详情:内部定制,初始化非空 */
  private avMediaDescription: avSession.AVMediaDescription = { assetId: '', startPosition: 0 };
  /** 缓存分辨率信息列表 */
  private m3u8InfoList: M3U8Info[] = [];
  
  /**
   * 解析清晰度码流
   */
  private parseUri(uri: string): M3U8Info[] {
    // 具体实现不在此详述
    return [];
  }

  /**
   * 业务注册获取媒体uri的函数:【使用投播必须获取媒体uri】
   *
   * @param callback 获取媒体uri的函数
   */
  registerGetMediaInfo(callback: () => CastMediaInfo): void {
    this.getMediaInfoFunction = callback;
  }

  /**
   * 开始投播
   */
  private startCast(): void {
    let mediaInfo: CastMediaInfo = this.getMediaInfoFunction();

    // 同步获取不同分辨率的二级索引url(获取到的url按清晰度从高到低排序)
    this.m3u8InfoList = this.parseUri(mediaInfo.url);

    // 设置默认推送的url(优先取720p,其次取清晰度最高的url,如4K或2K)
    let targetClarity = 'HD';
    // 根据默认的720p剧集的分辨率来获取
    for (const m3u8Info of this.m3u8InfoList) {
      if (m3u8Info.clarity === targetClarity) {
        // 推送的url
        this.avMediaDescription.mediaUri = m3u8Info.playUrl;
        break;
      }
    }
  }
}
  • 投送视频信息

获取到影片的URL后,通过[prepare]方法投送给播控中心,触发远端设备播放器进行投播。

prepare方法会投送一个播放列表AVQueueItem,AVQueueItem内容请参考[播放列表中单项的相关属性]。

调用prepare方法之后,APP播放框内就会展示成播放中的UI状态。

图4 APP播放框内投播UI状态

1

以下是具体的实现样例代码:

export class CastManager {
  /** 播控中心avSession */
  private avSession?: avSession.AVSession;
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  /** 获取媒体uri */
  private getMediaInfoFunction: () => CastMediaInfo = () => new CastMediaInfo();
  /** 媒体资源详情:内部定制,初始化非空 */
  private avMediaDescription: avSession.AVMediaDescription = { assetId: '', startPosition: 0 };
  /** 当前投播的媒体类型 */
  private currentCastMediaInfoType?: CastMediaInfoType;
  /** 开始投播的回调:用于刷新播窗ui,展示投播控制器ui */
  private callbackOnStart: (deviceName: string) => void;



  /**
   * 解析清晰度码流
   */
  private parseUri(uri: string): M3U8Info[] {
    // 具体实现不在此详述
    return [];
  }

  /**
   * 业务注册获取媒体uri的函数:【使用投播必须获取媒体uri】
   *
   * @param callback 获取媒体uri的函数
   */
  registerGetMediaInfo(callback: () => CastMediaInfo): void {
    this.getMediaInfoFunction = callback;
  }



  /**
   * 业务注册投播开始时回调
   *
   * @param callback 回调
   */
  onStart(callback: (deviceName: string) => void): void {
    this.callbackOnStart = callback;
  }

  /**
   * 开始投播
   */
  private async startCast(): Promise<void> {
    let mediaInfo: CastMediaInfo = this.getMediaInfoFunction();

    // 同步获取不同分辨率的二级索引url(获取到的url按清晰度从高到低排序)
    const m3u8InfoList = this.parseUri(mediaInfo.url);

    // 设置默认推送的url(优先取720p,其次取清晰度最高的url,如4K或2K)
    let targetClarity = 'HD';
    // 根据默认的720p剧集的分辨率来获取
    for (const m3u8Info of m3u8InfoList) {
      if (m3u8Info.clarity === targetClarity) {
        // 推送的url
        this.avMediaDescription.mediaUri = m3u8Info.playUrl;
        break;
      }
    }

    // 通知业务开始投播
    const deviceName: string = '客厅的智慧屏';
    this.callbackOnStart?.(deviceName);

    // 构建播放影片队列数据,开始prepare
    const queueItem = this.buildAVQueueItem();
    try {
      await this.avCastController?.prepare(queueItem);
    } catch (err) {
      this.handlerCastError(err, 'avSessionError', 'prepare');
    }

  }

  /**
   * 构建投播视频队列子项
   *
   * @returns 投播视频队列子项
   */
  private buildAVQueueItem(): avSession.AVQueueItem {
    hilog.info(0x0666, TAG, `buildAVQueueItem, description:${JSON.stringify(this.avMediaDescription)}`);
    // 构建媒体item
    let item: avSession.AVQueueItem = {
      itemId: 0,
      description: this.avMediaDescription
    };
    hilog.debug(0x0666, TAG, `buildAVQueueItem, queue: ${JSON.stringify(item)}`);
    return item;
  }

  /**
   * 将投播过程中的报错通知给投播组件展示,并打印日志
   * @param err 错误信息
   * @param type 错误类型
   * @param funcName 投播调用的函数名
   */
  private handlerCastError(err: BusinessError, type: CastErrorType, funcName: string): void {
    if (type === 'playVodError') {
      this.stopCast();
    }
    hilog.error(0x0666, TAG, `Failed to ${funcName}; errCode: ${err.code}, errType: ${type}`);
  }

  /**
   * 结束投播
   */
  private stopCast(): void {
    // 通知avSession结束投播
    this.avSession?.stopCasting();
  }
}
  • 视频在远端播放

在prepare回调成功之后,应用需要继续串行调用[start]接口通知远端进行启播。

start接口调用成功后,远端设备播放器就可以播放出流了。

图5 远端设备播放器

1

样例代码如下:

// CastManager.ets
export class CastManager {
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  /** 获取媒体uri */
  private getMediaInfoFunction: () => CastMediaInfo = () => new CastMediaInfo();
  /** 媒体资源详情:内部定制,初始化非空 */
  private avMediaDescription: avSession.AVMediaDescription = { assetId: '', startPosition: 0 };
  /** 当前投播的媒体类型 */
  private currentCastMediaInfoType?: CastMediaInfoType;
  /** 开始投播的回调:用于刷新播窗ui,展示投播控制器ui */
  private callbackOnStart: (deviceName: string) => void = () => {};
  /**
   * 开始投播
   */
  private async startCast(): Promise<void> {
    let mediaInfo: CastMediaInfo = this.getMediaInfoFunction();

    // 同步获取不同分辨率的二级索引url(获取到的url按清晰度从高到低排序)
    const m3u8InfoList = this.parseUri(mediaInfo.url);

    // 设置默认推送的url(优先取720p,其次取清晰度最高的url,如4K或2K)
    let targetClarity = 'HD';
    // 根据默认的720p剧集的分辨率来获取
    for (const m3u8Info of m3u8InfoList) {
      if (m3u8Info.clarity === targetClarity) {
        // 推送的url
        this.avMediaDescription.mediaUri = m3u8Info.playUrl;
        break;
      }
    }

    // 通知业务开始投播
    const deviceName: string = '客厅的智慧屏';
    this.callbackOnStart?.(deviceName);

    // 构建播放影片队列数据,开始prepare
    const queueItem = this.buildAVQueueItem();
    try {
      await this.avCastController.prepare(queueItem);
    } catch (err) {
      this.handlerCastError(err, 'avSessionError', 'prepare');
    }

    // 启动投播
    this.startPlay(mediaInfo.type);
  }

  /**
   * 构建投播视频队列子项
   *
   * @returns 投播视频队列子项
   */
  private buildAVQueueItem(): avSession.AVQueueItem {
    hilog.info(0x0666, TAG, `buildAVQueueItem, description:${JSON.stringify(this.avMediaDescription)}`);
    // 构建媒体item
    let item: avSession.AVQueueItem = {
      itemId: 0,
      description: this.avMediaDescription
    };
    hilog.debug(0x0666, TAG, `buildAVQueueItem, queue: ${JSON.stringify(item)}`);
    return item;
  }

  /**
   * 投播后设置监听器
   */
  private setListenerOnCast(type: 'online' | 'local'): void {
    // 稍后实现
  }

  /**
   * 通知远端开始播放
   *
   * @param type:起播类型:在线、本地
   */
  private startPlay(type: CastMediaInfoType): void {
    hilog.info(0x0666, TAG, `startPlay, type: ${type}`);
    if (!this.avCastController) {
      hilog.error(0x0666, TAG, 'startPlay, avCastController is undefined, can not startPlay');
      return;
    }

    // 构建播放影片队列数据
    const queueItem = this.buildAVQueueItem();
    this.avCastController?.start(queueItem)
      .then(() => {
        hilog.info(0x0666, TAG, 'success to avCastController.start');

        // 设置投播后的事件监听
        this.setListenerOnCast(this.avMediaDescription.mediaUri ? 'online' : 'local');

        // 更新当前投播的剧集信息
        this.currentCastMediaInfoType = type;
      })
      .catch((err: BusinessError) => {
        this.handlerCastError(err, 'avSessionError', 'start');
      });
  }
}
  • 告知播控中心华为视频支持的能力

投播时外部支持进行哪些操作,和之前对接AVSession控制一样,通过是否注册回调来控制。

对于不支持的操作,不需要注册回调,播控中心将不显示对应的操作按钮或将操作按钮置灰不可点击。

华为视频支持的操作:

  • 支持“下一首”,注册on(‘playNext’)事件。
  • 支持监听播放状态变化,注册on(‘playbackStateChange’)事件。
  • 支持SEEK进度,注册on(‘seekDone’)事件。
  • 支持展示AVSession的错误,注册on(‘error’)事件。
  • 支持获取影片时长,注册on(‘mediaItemChange’)事件。

华为视频不支持以下操作,将不注册对应回调:

  • 不支持收藏和循环模式,不注册on(‘toggleFavorite’)和on(‘setLoopMode’)事件。

  • 不支持上一集,不注册on(type: ‘playPrevious’, callback: Callback)事件。

  • 不支持video尺寸更改,不注册on(type: ‘videoSizeChange’)事件。

  • 拉起长时任务

投播在开始start之后,需要对接[申请长时任务],避免应用切后台之后被系统冻结,可以进行长期监控,完成连续播放。

需要注意如下几点:

  • 需要申请ohos.permission.KEEP_BACKGROUND_RUNNING权限。只需要申请本机权限即可。

  • 任务类型为:MULTI_DEVICE_CONNECTION。所有任务请查看[BackgroundMode]。

  • wantAgent参数用于点击长时任务后打开对应投播的详情页。

下面是开始和停止长时任务的示例代码:

// CastManager.ets
export class CastManager {
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  /** 媒体资源详情:内部定制,初始化非空 */
  private avMediaDescription: avSession.AVMediaDescription = { assetId: '', startPosition: 0 };
  /** 当前投播的媒体类型 */
  private currentCastMediaInfoType?: CastMediaInfoType;
  /** context,申请长时任务需要 */
  private context?: Context;

  /**
   * 通知远端开始播放
   *
   * @param type:起播类型:在线、本地
   */
  private startPlay(type: CastMediaInfoType): void {
    hilog.info(0x0666, TAG, `startPlay, type: ${type}`);
    if (!this.avCastController) {
      hilog.error(0x0666, TAG, 'startPlay, avCastController is undefined, can not startPlay');
      return;
    }

    // 构建播放影片队列数据
    const queueItem = this.buildAVQueueItem();
    this.avCastController?.start(queueItem)
      .then(() => {
        hilog.info(0x0666, TAG, 'success to avCastController.start');

        // 设置投播后的事件监听
        this.setListenerOnCast(this.avMediaDescription.mediaUri ? 'online' : 'local');

        // 更新当前投播的剧集信息
        this.currentCastMediaInfoType = type;

        // 申请长时任务
        this.startLongTimeTask();
      })
      .catch((err: BusinessError) => {
        this.handlerCastError(err, 'avSessionError', 'start');
      });
  }

  /**
   * 注册Context
   */
  registerContext(context: Context): void {
    this.context = context;
  }

  /**
   * 开始长时任务
   */
  private startLongTimeTask(): void {

    const wantAgentInfo: wantAgent.WantAgentInfo = {
      // 点击通知后,将要执行的动作列表
      wants: [
        {
          bundleName: 'com.huawei.hmsapp.himovie',
          abilityName: 'MainAbility',
          parameters: {
            type: 'avsession',
            category: this.currentCastMediaInfoType ?? '',
            routeParams: {
              vodId: this.avMediaDescription.assetId
            }
          }
        }
      ],
      // 点击通知后,动作类型
      operationType: wantAgent.OperationType.START_ABILITY,
      // 使用者自定义的一个私有值
      requestCode: 0,
      // 点击通知后,动作执行属性
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    this.startContinuousTask(this.context as Context,
      backgroundTaskManager.BackgroundMode.MULTI_DEVICE_CONNECTION,
      wantAgentInfo,() => {
        hilog.info(0x0666, TAG, 'success to startLongTimeTask.callback');
      });
  }

  /**
   * 开始长时任务
   *
   * @param context context
   * @param bgMode 后台模式
   * @param wantAgentInfo want信息
   * @param callbackOnStart 成功的回调
   */
  private startContinuousTask(context: Context, bgMode: backgroundTaskManager.BackgroundMode, wantAgentInfo: wantAgent.WantAgentInfo, callbackOnStart: () => void): void {
    // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
    wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
      backgroundTaskManager.startBackgroundRunning(context, bgMode, wantAgentObj)
        .then(callbackOnStart)
        .catch((err: BusinessError) => {
          hilog.error(0x0666, TAG, `Failed to operation startBackgroundRunning, code is ${err.code}`);
        });
    }).catch((err: BusinessError) => {
      hilog.error(0x0666, TAG, `Failed to start background running`);
    })
  }
}

下面是点击手机通知栏的长时任务时拉起影片详情页的相关代码:

// MainAbility.ets
export default class MainAbility extends UIAbility {
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 投播中,由长时任务拉起的事件
    if (want?.parameters?.type === 'avsession' && want?.parameters?.routeParams) {
      hilog.info(0x0666, TAG, `received a want, type is avsession, want.parameters.category: ${want.parameters?.category}`);
      router.pushUrl({
        url: '@bundle:com.huawei.hmsapp.himovie/local_video/ets/player/Player',
      }, router.RouterMode.Standard);
  }}
}

播控中心发起投播

点击控制中心的右上角的投播图标,会进入投音控制界面,在此界面中选择其他投播设备,可以进行设备连接投播。这个时候,播控中心会给业务应用回调通知,让应用接续完成投播能力。

图6 播控中心发起投播的流程
1

具体的实现原理如下:

  1. 用户选择设备且连接成功之后,播控中心触发on(‘outputDeviceChange’)回调通知应用,应用可感知到有投播设备正在试图连接。

  2. 后续流程与应用发起投播流程一致。可参考[华为视频应用内发起投播],大致分为:

    1. 根据ConnectionState判断是否连接成功。
    2. 根据DeviceInfo.castCategory判断是否远程设备。
    3. 根据DeviceInfo.supportedProtocols判断投播协议类型必须支持Cast+ Stream。
    4. 上述步骤均判断成功后,让应用请求投播播放URL,解析地址,投送URL。

连续播放影片的投播流程

对于连续播放,当前的方案如图所示。

图7 切换不同影片时播控中心显示

1

  • 如果应用仅在本地播放,播放状态上报给播控中心。(应用本地播放影片1)
  • 如果应用进行投播播放,则将投播状态上报到播控中心。(应用投播影片1)
  • 投播过程中,应用如果进行其他影片的本地播放,不会通知播控中心。(应用本地播放影片2,此时播控中心仍然显示影片1)
  • 当在投播过程中,本地播放的内容如果想触发投播时,需要通过播控中心提供的投播按钮实现。播控中心提供投播按钮的图片资源,应用内置做成按钮。播放框内点击这个按钮,直接将新内容的MetaData和投播URL都替换当前投播内容的方式实现。即对于播控中心,仅认为是投播中的内容变化了。
  • 投播和本地播放并存时,投播如果突然断开,当前播放的本地内容将立刻上报到播控中心。

播控状态显示和操作指令

  • 华为视频应用

    • 状态显示

图8 华为视频app内状态显示
2

通过监听on(type: ‘playbackStateChange’)事件,获取播放状态。

AVPlaybackState属性定义请参考[API文档]。

3

// CastManager.ets
import avSession from '@ohos.multimedia.avsession';
import hilog from '@ohos.hilog';
import type { BusinessError } from '@ohos.base';
import { CastMediaInfo, M3U8Info, CastMediaInfoType, CastErrorType } from './CastType';

const TAG = 'CastManager';
/**
 * 投播管理器
 */
export class CastManager {
  /** 单例 */
  private static readonly INSTANCE: CastManager = new CastManager();
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  /** 投播状态变化的回调 */
  private callbackOnPlaybackStateChange: (state: avSession.AVPlaybackState) => void = () => {};

  /**
   * 获取实例
   *
   * @returns 实例
   */
  public static getInstance(): CastManager {
    return CastManager.INSTANCE;
  }

  /**
   * 业务注册投播播放状态变化时回调
   *
   * @param callback 回调
   * @returns 实例
   */
  onPlaybackStateChange(callback: (state: avSession.AVPlaybackState) => void): void {
    this.callbackOnPlaybackStateChange = callback;
  }



  /**
   * 投播后设置监听器
   */
  private setListenerOnCast(type: 'online' | 'local'): void {
    // 播放状态变化
    this.setPlaybackStateChangeListener();
  }

  /**
   * 监听播控中心或者大屏的播放状态变化事件
   */
  private setPlaybackStateChangeListener(): void {
    this.avCastController?.on('playbackStateChange', 'all', playbackState => {
      // 通知业务播放状态变化
      this.callbackOnPlaybackStateChange(playbackState);
    });
  }
}
  • 操作指令

  • 播放和暂停

在进度条左边绘制播放/暂停按钮。

实际上华为视频应用无法直接控制远端影片播放的状态,必须通过播控中心来进行,状态的变化也依赖于播控中心的回调。

点击播放/暂停按钮后,调用[sendCommonCommand接口]通知播控中心进行状态变更。

为了尽快让用户看到状态的变化,在点击按钮后,立刻将状态设置成修改后的状态,之后播控中心再返回什么状态,就渲染成什么状态。

样例代码如下:

// CastManager.ets
export class CastManager {
  /** 单例 */
  private static readonly INSTANCE: CastManager = new CastManager();
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;

  /**
   * 获取实例
   *
   * @returns 实例
   */
  public static getInstance(): CastManager {
    return CastManager.INSTANCE;
  }

  /**
   * 发送控制指令
   *
   * @param controlParam 控制参数:控制类型、进度
   */
  sendControlCommand(command: avSession.AVCastControlCommand): Promise<void> {
    if (!this.avCastController) {
      return Promise.resolve();
    }
    hilog.info(0x0666, TAG, `sendControlCommand, command:${JSON.stringify(command)}`);
    return this.avCastController.sendControlCommand(command);
  }
}
// 业务Index.ets
@Entry
@Component
struct Index {
  private TAG = 'Index';
  /** 视频的总时长 */
  @State duration: number = 0;
  /** 是否在投播中 */
  @State isCasting: boolean = false;
  /** 当前播放进度 */
  @State currentTime: number = 0;
  /** 视频是否正在播放的用户状态,非实际状态 */
  @State isPlaying: boolean = false;

  /**
   * 播放/暂停按钮点击监听
   */
  private handlePlayPauseButtonClick(): void {
    if (this.isPlaying) {
      this.sendControlCommand('pause', this.currentTime, () => {
        this.isPlaying = false;
      });
    } else {
      this.sendControlCommand('play', this.currentTime, () => {
        this.isPlaying = true;
      });
    }
  }

  /**
   * 发送控制命令给播控中心
   * @param command 播控中心支持的控制命令
   * @param parameter 控制命令附带的参数
   * @param callback 执行成功后的回调
   */
  private sendControlCommand(command: avSession.AVCastControlCommandType, parameter: number, callback?: () => void): void {
    const controlParam: avSession.AVCastControlCommand = {
      command,
      parameter,
    };
    CastManager.getInstance().sendControlCommand(controlParam).then(() => {
      hilog.info(0x0666, this.TAG, `sendControlCommand set ${command} ok`);
      if (callback) {
        callback();
      }
    }).catch((err: BusinessError) => {
      hilog.error(0x0666, this.TAG, `sendControlCommand set ${command} fail, code: ${err.code}`);
    });
  }
}
  • 进度SEEK

  • 播放UI中需要根据上文的duration和position属性绘制播放进度条。每次回调时候进行刷新。没有触发回调时不显示进度。

  • 用户也可以自己点击拖动进度进行SEEK操作。当SEEK松手的时候,发送上述的命令提交SEEK进度。

  • SEEK命令发送之后,UI播控的进度和状态,等待SINK端的下次回调之后刷新。

  • 由于拖动进度条过程中,播控中心也会持续地返回进度给app,因此此时要禁用进度更新,防止进度条左右横跳。

样例代码如下:

// 业务Index.ets
/** 滑动条进度最大值 */
const MAX_SLIDER_VALUE = 100;

@Entry
@Component
struct Index {;
  private TAG = 'Index';
  /** 视频的总时长 */
  @State duration: number = 0;
  /** 是否在投播中 */
  @State isCasting: boolean = false;
  /** 当前播放进度 */
  @State currentTime: number = 0;
  /** 视频是否正在播放的用户状态,非实际状态 */
  @State isPlaying: boolean = false;

  /**
   * 进度条变化监听
   * @param value 新的值
   * @param mode 修改模式(Begin、End、Moving、Click)
   */
  private onSliderChange(value: number, mode: SliderChangeMode): void {
    if (this.duration) {
      this.currentTime = this.duration * value / MAX_SLIDER_VALUE;

      if (mode === SliderChangeMode.End) {
        this.sendControlCommand('seek', this.currentTime);
      }
    }
  }

  /**
   * 发送控制命令给播控中心
   * @param command 播控中心支持的控制命令
   * @param parameter 控制命令附带的参数
   * @param callback 执行成功后的回调
   */
  private sendControlCommand(command: avSession.AVCastControlCommandType, parameter: number, callback?: () => void): void {
    const controlParam: avSession.AVCastControlCommand = {
      command,
      parameter,
    };
    CastManager.getInstance().sendControlCommand(controlParam).then(() => {
      hilog.info(0x0666, this.TAG, `sendControlCommand set ${command} ok`);
      if (callback) {
        callback();
      }
    }).catch((err: BusinessError) => {
      hilog.error(0x0666, this.TAG, `sendControlCommand set ${command} fail, code: ${err.code}`);
    });
  }

  build() {
    // ...
  }
}
  • 自动下一集

图9 切换影片不同剧集时播控中心显示

4

影片播放结束以后,一般来说,电影是没有下一集,而电视剧是有下一集的,处理不同场景的处理方法如下:

对于切换不同电影、电视剧的场景

此时应该结束投播,华为视频应用内从刚刚播放的影片继续播放,远端播放器结束播放。

对于切换剧集(同一电视剧不同集或预告、花絮等)的场景

为了方便用户,此时不应该结束投播,应获取到下一集的播放URL,继续自动投播下一集。具体的实现如下:

  • 判断是否可以投播下一集:当前影片有下一集且华为视频应用感知到当前影片即将播放完毕,满足上述条件,即可投播下一集。

说明

华为视频app投播时判断影片即将播放完毕有两种方式:

  1. 收到avCastController的endOfStream回调时。
  2. 判断播放进度是否到了最后5秒以内。
  • 华为视频app播放详情页,状态切换到下一集。

  • 通知AvSession的Matedata信息,切换成下一个影片的信息。

  • 走华为视频app发起投播的流程,重新请求playVod,重新parepare和start启动投播。

样例代码如下:

// CastManager.ets
export class CastManager {
  /** 单例 */
  private static readonly INSTANCE: CastManager = new CastManager();
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  private context?: Context;

  /**
   * 投播后设置监听器
   */
  private setListenerOnCast(type: 'online' | 'local'): void {
    // 播放流结束
    this.setEndOfStreamListener();
  }

  /**
   * 监听播控中心或者大屏的endOfStream事件
   */
  private setEndOfStreamListener(): void {
    this.avCastController?.on('endOfStream', () => {
      // 通知页面播放下一集(页面处理逻辑不在此详述)
      this.context?.eventHub.emit('PLAY_COMPLETE');
    });
  }
}
  • 清晰度切换
  1. 开始投播的时候,多个清晰度的二级URL都已经解析好了。

用户选择清晰度后,就找到对应清晰度的二级URL然后调用prepare和start接口进行投播。

  1. 播放进度:按照当前触发切换时候的进度点开始继续播放。

样例代码:

// 业务Index.ets
@Entry
@Component
struct Index {
  private TAG = 'Index';
  /** 视频的总时长 */
  @State duration: number = 0;
  /** 是否在投播中 */
  @State isCasting: boolean = false;
  /** 当前播放进度 */
  @State currentTime: number = 0;
  /** 视频是否正在播放的用户状态,非实际状态 */
  @State isPlaying: boolean = false;
  /** 清晰度列表 */
  @State clarityInfoList: ClarityInfo[] = [];
  /** 选择的清晰度在列表中的下标 */
  @State selectedClarityIndex: number | undefined = undefined;
  /** 选择的清晰度的资源值:如:高清 */
  @State selectedClarityValue: string = '';

  /**
   * 清晰度选择Selector
   */
  @Builder
  ClaritySelector() {
    if (this.clarityInfoList && this.clarityInfoList.length > 0) {
      Select(this.clarityInfoList)
        .fontColor('#FFFFFF')
        .font({ size: 16 })
        .backgroundColor('#19FFFFFF')
        .borderRadius(20)
        .width(120)
        .height(40)
        .selected(this.selectedClarityIndex)
        .value(this.selectedClarityValue)
        .onSelect((index: number, text: string) => {
          hilog.info(0x0666, this.TAG, `select clarity, index:${index}, text:${text}`);
          if (this.selectedClarityIndex !== index) {
            this.selectedClarityIndex = index;
            this.selectedClarityValue = this.clarityInfoList[index].value;
            CastManager.getInstance().reStartCast(this.clarityInfoList[index].name);
          }
        })
        .id('id/cast_clarity_selector')
    }}
}

/**
 * 清晰度信息
 */
interface ClarityInfo {
  /**
   * 'HD' | 'SD' | 'BluRay'
   */
  name: string;

  /**
   * 展示名称,如中文:高清、标清、蓝光
   */
  value: string;
}
// CastManager.ets
export class CastManager {
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  /** 媒体资源详情:内部定制,初始化非空 */
  private avMediaDescription: avSession.AVMediaDescription = { assetId: '', startPosition: 0 };
  /** 缓存分辨率信息列表 */
  private m3u8InfoList: M3U8Info[] = [];

  /**
   * 重新投播:当前只有切换清晰度的场景会用
   *
   * @param clarity 清晰度
   */
  async reStartCast(clarity: string): Promise<void> {
    // 构建播放影片队列数据,开始prepare
    const queueItem = this.buildAVQueueItem();
    try {
      await this.avCastController?.prepare(queueItem);
    } catch (err) {
      this.handlerCastError(err, 'avSessionError', 'prepare');
    }

    let m3u8Info: M3U8Info | undefined = this.m3u8InfoList.find(item => item.clarity === clarity);
    if (!m3u8Info || !m3u8Info.playUrl) {
      hilog.error(0x0666, TAG, `Failed to find m3u8Info, when clarity is ${clarity}`);
      return;
    }
    // 更新播放url
    this.avMediaDescription.mediaUri = m3u8Info.playUrl;
    // 启动投播
    this.startPlay('online');
  }
}
  • 结束投播

  • 点击结束投播按钮,调用[AVSession.stopCasting接口]来结束投播;

  • 此时拉起APP内播放器进行播放,起播进度使用投播结束时的播放进度。

示例代码:

// 业务Index.ets
@Entry
@Component
struct Index {
  @Builder
  StopCastButton() {
    Button('结束投播')
      .fontColor('#FFFFFF')
      .fontSize(16)
      .backgroundColor('#19FFFFFF')
      .borderRadius(20)
      .width(120)
      .height(40)
      .onClick(() => {
        CastManager.getInstance().stopCast();
      })
      .id('id/cast_stop_button')
  }
}
  • 错误提示

  • 如果选择设备之后,在playVod播放鉴权的时候报错,根据错误码给对应提示。产生提示时,没有开始投播流程,也没有进入prepare状态,无法在投播界面展示,因此使用toast方式提示。

图10 播放鉴权报错时的错误提示
5

样例代码:

// CastManager.ets
export class CastManager {
  private getUIErrMessage(code: number): string {
    // 此处不详细写转换的逻辑
    return '';
  }

  /**
   * 将投播过程中的报错通知给投播组件展示,并打印日志
   * @param err 错误信息
   * @param type 错误类型
   * @param funcName 投播调用的函数名
   */
  private handlerCastError(err: BusinessError, type: CastErrorType, funcName: string): void {
    if (type === 'playVodError') {
      const message = this.getUIErrMessage(err.code);
      const toastString = `${message}(${err.code})`;
      hilog.warn(0x0666, TAG, `[handleCastError]playVodErrCode = ${err.code}, toastString = ${toastString}`);
      promptAction.showToast({
        message: toastString,
        duration: 3000,
      });
    }

    hilog.error(0x0666, TAG, `Failed to ${funcName}; errCode: ${err.code}, errType: ${type}`);
  }
}
  • 播控中心

控制中心的状态显示和操作指令,UI界面展示如下:

图11 播控中心状态显示
6

  • 操作路径

从系统顶部下拉进入控制中心 -> 点击影片海报进入播控中心页面 -> 点击右上角投播按钮进入投音控制页面;

从系统顶部下拉进入控制中心 -> 点击右上角投播按钮进入投音控制页面。

  • 状态显示

支持展示影片的标题、副标题、海报、播放状态、当前进度、总时长等信息。

  • 操作指令

支持播放暂停、进度SEEK、下一集、快进快退、调节SINK端音量、退出投播、切换设备等操作。

对以下场景,华为视频进行了监听处理:

  1. 播放暂停、进度SEEK、快进快退等状态监听,即监听playbackStateChange事件,详细代码可参考[华为视频应用-状态显示]。

  2. 下一集。

监听到下一集事件时,通知详情页组件播放下一集。此时会触发自动投播的流程,具体可参考[操作指令-自动下一集]。

  1. 播放结束。

播放结束后,和下一集的操作类似,可以选择结束投播,具体可以参考[操作指令-结束投播]。

  1. 退出投播、切换设备。

退出投播,其实就是切换到本机播放;切换设备,也是先切换到本机,再投播到其他设备,流程是类似的,都是监听outputDeviceChange事件,处理代码可以参考[使用隔空投放组件连接远端设备]。

  • 远端设备播放器

远端设备播放器应用状态显示和操作指令,其UI界面如下:

图12 大屏播放器播放状态显示
7

  • 状态显示

支持展示影片的标题、副标题、播放状态、当前进度、总时长等信息。

  • 操作指令

支持播放暂停、进度SEEK、下一集、快进快退、退出投播(关闭播放器)、设置播放速度等操作。

对播放暂停、进度SEEK、下一集、快进快退、退出投播场景,华为视频进行了监听处理,具体实现可以参考[华为视频应用-操作指令]。

本地视频支持投播

图13 本地视频投播
8

本地视频投播的逻辑和在线视频基本一致,具体差异点如下所示:

  • 不支持下一集、自动下一集。
  • 不支持清晰度切换。
  • 调用prepare接口时,不传mediaUri,传fdSrc(本地文件句柄media.AVFileDescriptor类型)。

完整示例代码

注意

代码目录结构如下所示,请开发者参考代码时注意文件的路径。

9

// MainAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AvSessionManager } from '../avsession/AvSessionManager';
import router from '@ohos.router';

const TAG = 'MainAbility';
export default class MainAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AvSessionManager.getInstance().init(this.context);
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 投播中,由长时任务拉起的事件
    if (want?.parameters?.type === 'avsession' && want?.parameters?.routeParams) {
      hilog.info(0x0666, TAG, `received a want, type is avsession, want.parameters.category: ${want.parameters?.category}`);
      router.pushUrl({
        url: '@bundle:com.huawei.hmsapp.himovie/local_video/ets/player/Player',
      }, router.RouterMode.Standard);
  }}
}
// CastType.ts
import media from '@ohos.multimedia.media';

/**
 * 媒体信息的类型:在线视频、本地视频
 */
export type CastMediaInfoType = 'online' | 'local';

/**
 * 媒体信息
 */
export class CastMediaInfo {
  /**
   * 媒体信息的类型
   * online:在线视频投播
   * local:本地视频投播
   */
  type: CastMediaInfoType;
  /**
   * vodId
   */
  vodId?: string;
  /**
   * 剧集id
   */
  volumeId?: string;
  /**
   * url
   */
  url: string;
  /**
   * 清晰度
   */
  clarity?: string;
  /**
   * 文件句柄
   */
  fdSrc?: media.AVFileDescriptor;
  /**
   * 展示错误类型
   */
  playErrType?: number;
  /**
   * 展示错误码
   */
  playErrCode?: number;
}

/**
 * 解析m3u8的信息
 */
export class M3U8Info {
  /**
   * 播放地址
   */
  playUrl?: string;
  /**
   * 带宽
   */
  bandwidth: number = Number.NaN;
  /**
   * 分辨率:0x0
   */
  resolution?: string;
  /**
   * 媒体分辨率:例如720、1080等,取高度
   */
  mediaResolution: number = Number.NaN;
  /**
   * 清晰度
   */
  clarity: string = '';
}

/**
 * 给页面返回的错误类型
 */
export type CastErrorType = 'avSessionError' | 'playVodError';
// 业务Index.ets
import avSession from '@ohos.multimedia.avsession';
import { AvSessionManager } from '../avsession/AvSessionManager';
import { CastManager } from '../avsession/CastManager';
import hilog from '@ohos.hilog';
import type { BusinessError } from '@ohos.base';

/** 滑动条进度最大值 */
const MAX_SLIDER_VALUE = 100;

@Entry
@Component
struct Index {
  private avsessionMetaData: avSession.AVMetadata | null = null;
  private TAG = 'Index';
  /** 视频的总时长 */
  @State duration: number = 0;
  /** 是否在投播中 */
  @State isCasting: boolean = false;
  /** 当前播放进度 */
  @State currentTime: number = 0;
  /** 视频是否正在播放的用户状态,非实际状态 */
  @State isPlaying: boolean = false;
  /** 投播的播放状态 */
  private playState: avSession.PlaybackState = avSession.PlaybackState.PLAYBACK_STATE_INITIAL;
  /** 清晰度列表 */
  @State clarityInfoList: ClarityInfo[] = [];
  /** 选择的清晰度在列表中的下标 */
  @State selectedClarityIndex: number | undefined = undefined;
  /** 选择的清晰度的资源值:如:高清 */
  @State selectedClarityValue: string = '';

  aboutToAppear(): void {
    CastManager.getInstance().registerContext(getContext());
    CastManager.getInstance().onPlaybackStateChange((avPlaybackState: avSession.AVPlaybackState) => {
      this.handlePlaybackStateChange(avPlaybackState);
    })
    this.setAVSessionMetaData();
  }

  /**
   * 投播播放状态变化监听
   * @param avPlaybackState 媒体播放状态相关属性
   */
  private handlePlaybackStateChange(avPlaybackState: avSession.AVPlaybackState): void {
    // 必须投播中才更新状态
    if (this.isCasting) {
      if (avPlaybackState?.state != null) {
        this.playState = avPlaybackState.state;
      }
      // 更新进度
      this.currentTime = avPlaybackState.position?.elapsedTime || 0;
      // 更新总时长
      if (avPlaybackState.extras?.duration && avPlaybackState.extras?.duration !== this.duration) {
        hilog.info(0x0666, this.TAG, `[handlePlaybackStateChange]duration set to ${this.duration}`);
        this.duration = avPlaybackState.extras?.duration as number;
      }
      // 更新播放状态
      this.isPlaying = this.playState === avSession.PlaybackState.PLAYBACK_STATE_PLAY;

      hilog.debug(0x0666, this.TAG, `avPlaybackState: ${JSON.stringify(avPlaybackState)}`);
    }
  }

  setAVSessionMetaData() {
    this.avsessionMetaData = {
      // 影片的id
      assetId: 'test vod id',
      subtitle: 'vod subtitle',
      artist: 'artist name',
      title: 'vod title',
      mediaImage: 'media image url',
      // 仅支持投屏到Cast+ Stream的设备
      filter: avSession.ProtocolType.TYPE_CAST_PLUS_STREAM,
      // 快进快退时间
      skipIntervals: avSession?.SkipIntervals?.SECONDS_30
    };
    AvSessionManager.getInstance().setMetaData(this.avsessionMetaData);
  }

  /**
   * 播放/暂停按钮点击监听
   */
  private handlePlayPauseButtonClick(): void {
    if (this.isPlaying) {
      this.sendControlCommand('pause', this.currentTime, () => {
        this.isPlaying = false;
      });
    } else {
      this.sendControlCommand('play', this.currentTime, () => {
        this.isPlaying = true;
      });
    }
  }

  /**
   * 进度条变化监听
   * @param value 新的值
   * @param mode 修改模式(Begin、End、Moving、Click)
   */
  private onSliderChange(value: number, mode: SliderChangeMode): void {
    if (this.duration) {
      this.currentTime = this.duration * value / MAX_SLIDER_VALUE;

      if (mode === SliderChangeMode.End) {
        this.sendControlCommand('seek', this.currentTime);
      }
    }
  }

  /**
   * 发送控制命令给播控中心
   * @param command 播控中心支持的控制命令
   * @param parameter 控制命令附带的参数
   * @param callback 执行成功后的回调
   */
  private sendControlCommand(command: avSession.AVCastControlCommandType, parameter: number, callback?: () => void): void {
    const controlParam: avSession.AVCastControlCommand = {
      command,
      parameter,
    };
    CastManager.getInstance().sendControlCommand(controlParam).then(() => {
      hilog.info(0x0666, this.TAG, `sendControlCommand set ${command} ok`);
      if (callback) {
        callback();
      }
    }).catch((err: BusinessError) => {
      hilog.error(0x0666, this.TAG, `sendControlCommand set ${command} fail, code: ${err.code}`);
    });
  }

  /**
   * 清晰度选择Selector
   */
  @Builder
  ClaritySelector() {
    if (this.clarityInfoList && this.clarityInfoList.length > 0) {
      Select(this.clarityInfoList)
        .fontColor('#FFFFFF')
        .font({ size: 16 })
        .backgroundColor('#19FFFFFF')
        .borderRadius(20)
        .width(120)
        .height(40)
        .selected(this.selectedClarityIndex)
        .value(this.selectedClarityValue)
        .onSelect((index: number, text: string) => {
          hilog.info(0x0666, this.TAG, `select clarity, index:${index}, text:${text}`);
          if (this.selectedClarityIndex !== index) {
            this.selectedClarityIndex = index;
            this.selectedClarityValue = this.clarityInfoList[index].value;
            CastManager.getInstance().reStartCast(this.clarityInfoList[index].name);
          }
        })
        .id('id/cast_clarity_selector')
    }
  }

  @Builder
  StopCastButton() {
    Button('结束投播')
      .fontColor('#FFFFFF')
      .fontSize(16)
      .backgroundColor('#19FFFFFF')
      .borderRadius(20)
      .width(120)
      .height(40)
      .onClick(() => {
        CastManager.getInstance().stopCast();
      })
      .id('id/cast_stop_button')
  }

  build() {
    // ...
  }
}



/**
 * 清晰度信息
 */
interface ClarityInfo {
  /**
   * 'HD' | 'SD' | 'BluRay'
   */
  name: string;

  /**
   * 展示名称,如中文:高清、标清、蓝光
   */
  value: string;
}
// CastManager.ets
import { avSession } from '@kit.AVSessionKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { wantAgent } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { CastMediaInfo, M3U8Info, CastMediaInfoType, CastErrorType } from './CastType';

const TAG = 'CastManager';

/**
 * 投播管理器
 */
export class CastManager {
  /** 单例 */
  private static readonly INSTANCE: CastManager = new CastManager();
  /** 播控中心avSession */
  private avSession?: avSession.AVSession;
  /** 投播控制器 */
  private avCastController?: avSession.AVCastController;
  /** 获取媒体uri */
  private getMediaInfoFunction: () => CastMediaInfo = () => new CastMediaInfo();
  /** 媒体资源详情:内部定制,初始化非空 */
  private avMediaDescription: avSession.AVMediaDescription = { assetId: '', startPosition: 0 };
  /** 当前投播的媒体类型 */
  private currentCastMediaInfoType?: CastMediaInfoType;
  /** 开始投播的回调:用于刷新播窗ui,展示投播控制器ui */
  private callbackOnStart: (deviceName: string) => void = () => {};
  /** 投播状态变化的回调 */
  private callbackOnPlaybackStateChange: (state: avSession.AVPlaybackState) => void = () => {};
  /** 播放结束的回调 */
  private callbackOnEndOfStream: () => void = () => {};
  private context?: Context;
  /** 缓存分辨率信息列表 */
  private m3u8InfoList: M3U8Info[] = [];

  /**
   * 获取实例
   *
   * @returns 实例
   */
  public static getInstance(): CastManager {
    return CastManager.INSTANCE;
  }

  public afterCreateSession(session: avSession.AVSession) {
    this.avSession = session;
    // 监听设备连接状态的变化
    this.setOutputDeviceChangeListener();
  }

  /**
   * 设置输出设备变化监听器
   */
  private setOutputDeviceChangeListener(): void {
    this.avSession?.on('outputDeviceChange', (connectState: avSession.ConnectionState,
      device: avSession.OutputDeviceInfo) => {
      const castCategory = device?.devices?.[0].castCategory;
      // 成功连接远程设备
      if (castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_CONNECTED) {
        // 获取cast控制器
        this.avSession?.getAVCastController().then(async (controller: avSession.AVCastController) => {
          hilog.info(0x0666, TAG, 'success to get avController');
          this.avCastController = controller;
          this.startCast();
        })
      }

      // 远端断开 或 本地连上 都算断开投播
      const isDisConnect = (castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_DISCONNECTED)
        || (castCategory === avSession.AVCastCategory.CATEGORY_LOCAL && connectState === avSession.ConnectionState.STATE_CONNECTED);
      if (isDisConnect) {
        this.stopCast();
      }
    });
  }

  /**
   * 解析清晰度码流
   */
  private parseUri(uri: string): M3U8Info[] {
    // 具体实现不在此详述
    return [];
  }

  /**
   * 业务注册获取媒体uri的函数:【使用投播必须获取媒体uri】
   *
   * @param callback 获取媒体uri的函数
   */
  registerGetMediaInfo(callback: () => CastMediaInfo): void {
    this.getMediaInfoFunction = callback;
  }



  /**
   * 业务注册投播开始时回调
   *
   * @param callback 回调
   */
  onStart(callback: (deviceName: string) => void): void {
    this.callbackOnStart = callback;
  }

  /**
   * 业务注册投播播放状态变化时回调
   *
   * @param callback 回调
   * @returns 实例
   */
  onPlaybackStateChange(callback: (state: avSession.AVPlaybackState) => void): void {
    this.callbackOnPlaybackStateChange = callback;
  }

  /**
   * 播放结束
   *
   * @param callback 回调
   * @returns this
   */
  onEndOfStream(callback: () => void): CastManager {
    this.callbackOnEndOfStream = callback;
    return this;
  }

  /**
   * 开始投播
   */
  private async startCast(): Promise<void> {
    let mediaInfo: CastMediaInfo = this.getMediaInfoFunction();

    // 同步获取不同分辨率的二级索引url(获取到的url按清晰度从高到低排序)
    this.m3u8InfoList = this.parseUri(mediaInfo.url);

    // 设置默认推送的url(优先取720p,其次取清晰度最高的url,如4K或2K)
    let targetClarity = 'HD';
    // 根据默认的720p剧集的分辨率来获取
    for (const m3u8Info of this.m3u8InfoList) {
      if (m3u8Info.clarity === targetClarity) {
        // 推送的url
        this.avMediaDescription.mediaUri = m3u8Info.playUrl;
        break;
      }
    }

    // 通知业务开始投播
    const deviceName: string = '客厅的智慧屏';
    this.callbackOnStart?.(deviceName);

    // 构建播放影片队列数据,开始prepare
    const queueItem = this.buildAVQueueItem();
    try {
      await this.avCastController?.prepare(queueItem);
    } catch (err) {
      this.handlerCastError(err, 'avSessionError', 'prepare');
    }

    // 启动投播
    this.startPlay(mediaInfo.type);
  }

  /**
   * 构建投播视频队列子项
   *
   * @returns 投播视频队列子项
   */
  private buildAVQueueItem(): avSession.AVQueueItem {
    hilog.info(0x0666, TAG, `buildAVQueueItem, description:${JSON.stringify(this.avMediaDescription)}`);
    // 构建媒体item
    let item: avSession.AVQueueItem = {
      itemId: 0,
      description: this.avMediaDescription
    };
    hilog.debug(0x0666, TAG, `buildAVQueueItem, queue: ${JSON.stringify(item)}`);
    return item;
  }

  /**
   * 投播后设置监听器
   */
  private setListenerOnCast(type: 'online' | 'local'): void {
    // 播放状态变化
    this.setPlaybackStateChangeListener();
    // 播放流结束
    this.setEndOfStreamListener();
  }



  /**
   * 监听播控中心或者大屏的endOfStream事件
   */
  private setEndOfStreamListener(): void {
    this.avCastController?.on('endOfStream', () => {
      // 通知页面播放下一集
      this.context?.eventHub.emit('PLAY_COMPLETE');
    });
  }

  /**
   * 监听播控中心或者大屏的播放状态变化事件
   */
  private setPlaybackStateChangeListener(): void {
    this.avCastController?.on('playbackStateChange', 'all', playbackState => {
      // 通知业务播放状态变化
      this.callbackOnPlaybackStateChange(playbackState);
    });
  }

  private getUIErrMessage(code: number): string {
    // 此处不详细写转换的逻辑
    return '';
  }

  /**
   * 将投播过程中的报错通知给投播组件展示,并打印日志
   * @param err 错误信息
   * @param type 错误类型
   * @param funcName 投播调用的函数名
   */
  private handlerCastError(err: BusinessError, type: CastErrorType, funcName: string): void {
    if (type === 'playVodError') {
      const message = this.getUIErrMessage(err.code);
      const toastString = `${message}(${err.code})`;
      hilog.warn(0x0666, TAG, `[handleCastError]playVodErrCode = ${err.code}, toastString = ${toastString}`);
      promptAction.showToast({
        message: toastString,
        duration: 3000,
      });
    }

    hilog.error(0x0666, TAG, `Failed to ${funcName}; errCode: ${err.code}, errType: ${type}`);
  }

  /**
   * 结束投播
   */
  public stopCast(): void {
    // 通知avSession结束投播
    this.avSession?.stopCasting();
  }

  /**
   * 通知远端开始播放
   *
   * @param type:起播类型:在线、本地
   */
  private startPlay(type: CastMediaInfoType): void {
    hilog.info(0x0666, TAG, `startPlay, type: ${type}`);
    if (!this.avCastController) {
      hilog.error(0x0666, TAG, 'startPlay, avCastController is undefined, can not startPlay');
      return;
    }

    // 构建播放影片队列数据
    const queueItem = this.buildAVQueueItem();
    this.avCastController?.start(queueItem)
      .then(() => {
        hilog.info(0x0666, TAG, 'success to avCastController.start');

        // 设置投播后的事件监听
        this.setListenerOnCast(this.avMediaDescription.mediaUri ? 'online' : 'local');

        // 更新当前投播的剧集信息
        this.currentCastMediaInfoType = type;

        // 申请长时任务
        this.startLongTimeTask();
      })
      .catch((err: BusinessError) => {
        this.handlerCastError(err, 'avSessionError', 'start');
      });
  }

  /**
   * 发送控制指令
   *
   * @param controlParam 控制参数:控制类型、进度
   */
  sendControlCommand(command: avSession.AVCastControlCommand): Promise<void> {
    if (!this.avCastController) {
      return Promise.resolve();
    }
    hilog.info(0x0666, TAG, `sendControlCommand, command:${JSON.stringify(command)}`);
    return this.avCastController.sendControlCommand(command);
  }

  /**
   * 重新投播:当前只有切换清晰度的场景会用
   *
   * @param clarity 清晰度
   */
  async reStartCast(clarity: string): Promise<void> {
    // 构建播放影片队列数据,开始prepare
    const queueItem = this.buildAVQueueItem();
    try {
      await this.avCastController?.prepare(queueItem);
    } catch (err) {
      this.handlerCastError(err, 'avSessionError', 'prepare');
    }

    let m3u8Info: M3U8Info | undefined = this.m3u8InfoList.find(item => item.clarity === clarity);
    if (!m3u8Info || !m3u8Info.playUrl) {
      hilog.error(0x0666, TAG, `Failed to find m3u8Info, when clarity is ${clarity}`);
      return;
    }
    // 更新播放url
    this.avMediaDescription.mediaUri = m3u8Info.playUrl;
    // 启动投播
    this.startPlay('online');
  }

  /**
   * 注册Context
   */
  registerContext(context: Context): void {
    this.context = context;
  }

  /**
   * 开始长时任务
   */
  private startLongTimeTask(): void {

    const wantAgentInfo: wantAgent.WantAgentInfo = {
      // 点击通知后,将要执行的动作列表
      wants: [
        {
          bundleName: 'com.huawei.hmsapp.himovie',
          abilityName: 'MainAbility',
          parameters: {
            type: 'avsession',
            category: this.currentCastMediaInfoType ?? '',
            routeParams: {
              vodId: this.avMediaDescription.assetId
            }
          }
        }
      ],
      // 点击通知后,动作类型
      operationType: wantAgent.OperationType.START_ABILITY,
      // 使用者自定义的一个私有值
      requestCode: 0,
      // 点击通知后,动作执行属性
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    this.startContinuousTask(this.context as Context,
      backgroundTaskManager.BackgroundMode.MULTI_DEVICE_CONNECTION,
      wantAgentInfo,() => {
        hilog.info(0x0666, TAG, 'success to startLongTimeTask.callback');
      });
  }

  /**
   * 开始长时任务
   *
   * @param context context
   * @param bgMode 后台模式
   * @param wantAgentInfo want信息
   * @param callbackOnStart 成功的回调
   */
  private startContinuousTask(context: Context, bgMode: backgroundTaskManager.BackgroundMode, wantAgentInfo: wantAgent.WantAgentInfo, callbackOnStart: () => void): void {
    // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
    wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
      backgroundTaskManager.startBackgroundRunning(context, bgMode, wantAgentObj)
        .then(callbackOnStart)
        .catch((err: BusinessError) => {
          hilog.error(0x0666, TAG, `Failed to operation startBackgroundRunning, code is ${err.code}`);
        });
    }).catch((err: BusinessError) => {
      hilog.error(0x0666, TAG, `Failed to start background running`);
    })
  }
}
// AvSessionManager.ts
import avSession from '@ohos.multimedia.avsession';
import hilog from '@ohos.hilog';
import type { BusinessError } from '@ohos.base';
import type common from '@ohos.app.ability.common';
import WantAgent from '@ohos.app.ability.wantAgent';

const TAG = 'AvSessionManager';

/**
 * 对接播控中心管理器
 */
export class AvSessionManager {

  private static readonly instance: AvSessionManager = new AvSessionManager();
  private session: avSession.AVSession = null;
  /** 播放状态 */
  playState?: avSession.AVPlaybackState = {
    state: avSession.PlaybackState.PLAYBACK_STATE_INITIAL,
    position: {
      elapsedTime: 0,
      updateTime: (new Date()).getTime()
    }
  };

  static getInstance(): AvSessionManager {
    return this.instance;
  }

  init(abilityContext: common.Context): void {
    avSession.createAVSession(abilityContext, 'himovie', 'video').then(session => {
      this.session = session;
      // 创建完成之后,激活会话。
      this.session.activate();
      hilog.info(0x06666, TAG, 'createAVSession success');
    }).catch((error: BusinessError) => {
      hilog.error(0x06666, TAG, `createAVSession or activate failed, code: ${error?.code}`);
    });
  }

  /**
   * 设置metaData并初始化状态
   *
   * @param metadata 影片元数据
   */
  setMetaData(metadata: avSession.AVMetadata): void {
    if (this.session) {
      hilog.info(0x06666, TAG, `setMetaData avMetadata: ${JSON.stringify(metadata)}`);

      this.session?.setAVMetadata(metadata)
        .then(() => {
          hilog.info(0x06666, TAG, `setMetaData success.`);
          this.setLaunchAbility(metadata.assetId);
        })
        .catch((error: BusinessError) => {
          hilog.error(0x06666, TAG, `setMetaData failed, code: ${error.code}`);
        });
    }
  }

  /**
   * 设置一个WantAgent用于拉起会话的Ability
   * @param vodId 影片Id
   */
  setLaunchAbility(vodId: string): void {
    const ability: WantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'com.huawei.hmsapp.himovie',
          abilityName: 'MainAbility',
          parameters: {
            type: 'avsession',
            routeParams: {
              vodId,
            }
          }
        }
      ],
      requestCode: 0,
      actionType: WantAgent.OperationType.START_ABILITY,
      actionFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    }
    this.session.setLaunchAbility(ability).then(() => {
      hilog.info(0x0666, TAG, `SetLaunchAbility successfully`);
    }).catch((err: BusinessError) => {
      hilog.info(0x0666, TAG, `SetLaunchAbility failed, code: ${err.code}`);
    });
  }

  /**
   * 播放
   *
   * @returns
   */
  play(currentTime?: number): void {
    hilog.info(0x0666, TAG, `AVSession play, currentTime:${currentTime}, state: ${this.playState?.state}`);
    this.setPlayOrPauseToAvSession('play', currentTime);
  }

  /**
   * 暂停
   *
   * @returns
   */
  pause(currentTime?: number): void {
    hilog.info(0x0666, TAG, `AVSession pause, currentTime: ${currentTime}, state: ${this.playState?.state}`);
    this.setPlayOrPauseToAvSession('pause', currentTime);
  }



  /**
   * 设置播控中心的状态为播放或暂停
   *
   * @param state 状态
   * @param elapsedTime 当前进度
   */
  private setPlayOrPauseToAvSession(state: 'play' | 'pause', elapsedTime: number): void {
    if (elapsedTime === undefined || elapsedTime < 0) {
      hilog.warn(0x0666, TAG, `param error, elapsedTime: ${elapsedTime}, do not play or pause.`);
      return;
    }
    if (this.playState === undefined || this.playState.state === avSession.PlaybackState.PLAYBACK_STATE_STOP) {
      hilog.warn(0x0666, TAG, `playState error, state is PLAYBACK_STATE_STOP or undefined, do not play or pause.`);
      return;
    }

    this.playState.state = state === 'play' ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE;
    this.playState.position = {
      elapsedTime: elapsedTime,
      updateTime: (new Date()).getTime()
    };

    this.setAVPlaybackState();
  }

  /**
   * 向播控中心设置播放状态
   */
  private setAVPlaybackState(): void {
    hilog.info(0x0666, TAG, `setAVPlaybackState state: ${this.playState.state}, updateTime: ${this.playState?.position?.updateTime}, speed: ${this.playState?.speed}`);
    this.session?.setAVPlaybackState(this.playState);
  }

  /**
   * 释放播放器
   */
  releasePlayer(): void {
    this.playState.state = avSession.PlaybackState.PLAYBACK_STATE_STOP;
    this.setAVPlaybackState();
  }



  /**
   * 监听播控中心回调事件,播放
   *
   * @param action 回调方法
   * @returns
   */
  onPlay(action: () => void): void {
    if (this.session) {
      this.session.on('play', action);
    }
  }

  /**
   * 监听播控中心回调事件,暂停
   *
   * @param action 回调方法
   */
  onPause(action: () => void): void {
    if (this.session) {
      this.session.on('pause', action);
    }
  }
}

最后呢

很多开发朋友不知道需要学习那些鸿蒙技术?鸿蒙开发岗位需要掌握那些核心技术点?为此鸿蒙的开发学习必须要系统性的进行。

而网上有关鸿蒙的开发资料非常的少,假如你想学好鸿蒙的应用开发与系统底层开发。你可以参考这份资料,少走很多弯路,节省没必要的麻烦。由两位前阿里高级研发工程师联合打造的《鸿蒙NEXT星河版OpenHarmony开发文档》里面内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战等等)鸿蒙(Harmony NEXT)技术知识点

如果你是一名Android、Java、前端等等开发人员,想要转入鸿蒙方向发展。可以直接领取这份资料辅助你的学习。下面是鸿蒙开发的学习路线图。

在这里插入图片描述

针对鸿蒙成长路线打造的鸿蒙学习文档。话不多说,我们直接看详细鸿蒙(OpenHarmony )手册(共计1236页)与鸿蒙(OpenHarmony )开发入门视频,帮助大家在技术的道路上更进一步。

  • 《鸿蒙 (OpenHarmony)开发学习视频》
  • 《鸿蒙生态应用开发V2.0白皮书》
  • 《鸿蒙 (OpenHarmony)开发基础到实战手册》
  • OpenHarmony北向、南向开发环境搭建
  • 《鸿蒙开发基础》
  • 《鸿蒙开发进阶》
  • 《鸿蒙开发实战》

在这里插入图片描述

总结

鸿蒙—作为国家主力推送的国产操作系统。部分的高校已经取消了安卓课程,从而开设鸿蒙课程;企业纷纷跟进启动了鸿蒙研发。

并且鸿蒙是完全具备无与伦比的机遇和潜力的;预计到年底将有 5,000 款的应用完成原生鸿蒙开发,未来将会支持 50 万款的应用。那么这么多的应用需要开发,也就意味着需要有更多的鸿蒙人才。鸿蒙开发工程师也将会迎来爆发式的增长,学习鸿蒙势在必行! 自↓↓↓拿

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值