当前位置: 首页 > news >正文

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

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

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

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

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

华为视频接入播控中心

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

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

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

1

华为视频同步播控中心

说明

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

  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 = {// 影片的idassetId: '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) {// 推送的urlthis.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) {// 推送的urlthis.avMediaDescription.mediaUri = m3u8Info.playUrl;break;}}// 通知业务开始投播const deviceName: string = '客厅的智慧屏';this.callbackOnStart?.(deviceName);// 构建播放影片队列数据,开始prepareconst 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)}`);// 构建媒体itemlet 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) {// 推送的urlthis.avMediaDescription.mediaUri = m3u8Info.playUrl;break;}}// 通知业务开始投播const deviceName: string = '客厅的智慧屏';this.callbackOnStart?.(deviceName);// 构建播放影片队列数据,开始prepareconst 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)}`);// 构建媒体itemlet 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*/@BuilderClaritySelector() {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> {// 构建播放影片队列数据,开始prepareconst 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;}// 更新播放urlthis.avMediaDescription.mediaUri = m3u8Info.playUrl;// 启动投播this.startPlay('online');}
}
  • 结束投播

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

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

示例代码:

// 业务Index.ets
@Entry
@Component
struct Index {@BuilderStopCastButton() {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 = {// 影片的idassetId: '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*/@BuilderClaritySelector() {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')}}@BuilderStopCastButton() {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) {// 推送的urlthis.avMediaDescription.mediaUri = m3u8Info.playUrl;break;}}// 通知业务开始投播const deviceName: string = '客厅的智慧屏';this.callbackOnStart?.(deviceName);// 构建播放影片队列数据,开始prepareconst 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)}`);// 构建媒体itemlet 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> {// 构建播放影片队列数据,开始prepareconst 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;}// 更新播放urlthis.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 万款的应用。那么这么多的应用需要开发,也就意味着需要有更多的鸿蒙人才。鸿蒙开发工程师也将会迎来爆发式的增长,学习鸿蒙势在必行! 自↓↓↓拿

相关文章:

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

华为视频接入播控中心和投播能力概述** 华为视频在进入影片详情页播放时&#xff0c;支持在控制中心查看当前播放的视频信息&#xff0c;并进行快进、快退、拖动进度、播放暂停、下一集、调节音量等操作&#xff0c;方便用户通过控制中心来操作当前播放的视频。 当用户希望通…...

【STM32项目】在FreeRtos背景下的实战项目的实现过程(一)

个人主页~ 这篇文章是我亲身经历的&#xff0c;在做完一个项目之后总结的经验&#xff0c;虽然我没有将整个项目给放出来&#xff0c;因为这项目确实也是花了米让导师指导的&#xff0c;但是这个过程对于STM32的实战项目开发都是非常好用的&#xff0c;可以说按照这个过程&…...

C#垃圾处理机制相关笔记

C#编程中的垃圾处理机制主要通过垃圾回收器&#xff08;Garbage Collector&#xff0c;GC&#xff09;实现自动内存管理。C#作为一种托管语言&#xff0c;其垃圾处理机制显著减轻了程序员的内存管理负担&#xff0c;与C语言等非托管语言形成鲜明对比。具体介绍如下&#xff1a;…...

C语言memcmp函数

目录 开头1.什么是memcmp函数?2.memcmp函数的内部程序流程图 3.memcmp函数的实际应用比较整型数组比较短整型二维数组比较结构体变量…… 结尾 开头 大家好&#xff0c;我叫这是我58。今天&#xff0c;我们要学一下关于C语言里的memcmp函数的一些知识。 1.什么是memcmp函数?…...

低代码: 组件库测试之Vue环境下的测试工具以及测试环境搭建

Vue Test Utils Vue Test Utils 1 targets Vue 2. Vue Test Utils 2 targets Vue 3. 特别注意要使用 版本 2.0.0 以上 提供特定的方法,在隔离的话环境下,进行组件的挂载,以及一系列的测试 配置开发环境 手动配置, 是比较麻烦的vue cli 是基于插件架构的, 插件可以: 安装对…...

【Vue3】高颜值后台管理模板推荐

ELP - 权限管理系统 基于Vue 3框架与PrimeVue UI组件库技术精心构建的高颜值后台权限管理系统模板。该模板系统已成功实现基于RBAC&#xff08;Role-Based Access Control&#xff09;模型的权限管理系统和字典数据管理模块&#xff0c;后端则使用了Spring Boot框架&#xff0…...

详细介绍Pytorch中torchvision的相关使用

torchvision 是 PyTorch 的一个官方库&#xff0c;主要用于处理计算机视觉任务。提供了许多常用的数据集、模型架构、图像转换等功能&#xff0c;使得计算机视觉任务的开发变得更加高效和便捷。以下是对 torchvision 主要功能的详细介绍&#xff1a; 1. 数据集&#xff08;Dat…...

AI部署——主流模型推理部署框架

我们以最经典的Yolov5目标检测网络为例解释一下10种主流推理部署框架的大概内容&#xff0c;省略模型训练的过程&#xff0c;只讨论模型转换、环境配置、推理部署等步骤。 Intel的OpenVINO — CPUNvidia的TensorRT — GPU/CPUOpenCV DNN Module — GPU/CPUMicrosoft ONNX Runti…...

PyTorch之loading fbgemm.dll异常的解决办法

前言 PyTorch是一个深度学习框架&#xff0c;当我们在本地调试大模型时&#xff0c;可能会选用并安装它&#xff0c;目前已更新至2.4版本。 一、安装必备 1. window 学习或开发阶段&#xff0c;我们通常在window环境下进行&#xff0c;因此需满足以下条件&#xff1a; Windo…...

Vscode——如何实现 Ctrl+鼠标左键 跳转函数内部的方法

一、对于Python代码 安装python插件即可实现 二、对于C/C代码 安装C/C插件即可实现...

力扣热题100_回溯_78_子集

文章目录 题目链接解题思路解题代码 题目链接 78. 子集 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff…...

浏览器如何工作(一)进程架构

分享cosine 大佬&#xff0c;版权©️大佬所有 浏览器的核心功能 浏览器&#xff0c;“浏览” 是这个产品的核心&#xff0c;浏览无非分为两步&#xff1a; 获取想浏览的资源 展示得到的资源 现代浏览器还增加了交互功能&#xff0c;这涉及到脚本运行。因此&#xff0c…...

【LeetCode】两数之和

给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。 示例 1…...

UE5学习笔记11-为拿取武器添加动画

一、一点说明 动画实例通过扩展为所有机器上的每个字符都存在动画蓝图&#xff0c;动画实例只能访问该计算机上的变量。 二、思路 我在武器组件中有一个武器类的指针&#xff0c;判断当前指针是否为空去判断当前角色是否装备武器 三、实现 1.在角色C类中添加是否装备武器的函…...

68. 文本左右对齐【 力扣(LeetCode) 】

一、题目描述 给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词&#xff1b;也就是说&#xff0c;尽可能多地往每行中放置单…...

【中等】 猿人学web第一届 第6题 js混淆-回溯

文章目录 请求流程请求参数 加密参数定位r() 方法z() 方法 加密参数还原JJENCOde js代码加密环境检测_n("jsencrypt")12345 计算全部中奖的总金额请求代码注意 请求流程 请求参数 打开 调试工具&#xff0c;查看数据接口 https://match.yuanrenxue.cn/api/match/6 请…...

低、中、高频率段具体在不同应用中的范围是多少

1、低频率段&#xff08;Low Frequency Range&#xff09; ①建筑声学和噪声控制&#xff1a;通常将20 Hz 到 200 Hz 的频率范围视为低频段。在这一范围内&#xff0c;声音的波长较长&#xff0c;通常与低音&#xff08;如重低音音乐&#xff09;和建筑结构中的振动有关。 ②…...

Oxford Model600 Model400低温氦压缩机cryogenic helium compressor手侧

Oxford Model600 Model400低温氦压缩机cryogenic helium compressor手侧...

Golang面试题四(并发编程)

目录 1.Go常见的并发模型 2.哪些方法安全读写共享变量 3.如何排查数据竞争问题 ​4.Go有哪些同步原语 1. Mutex (互斥锁) 2. RWMutex (读写互斥锁) 3. Atomic 3.1.使用场景 3.2.整型操作 3.3.指针操作 3.4.使用示例 4. Channel 使用场景 使用示例 5. sync.WaitGr…...

计算机学生高效记录并整理编程学习笔记的方法

哪些知识点需要做笔记&#xff1f; 以下是我认为计算机学生大学四年可以积累的笔记。 ① 编程语言类&#xff08;C语言CJava&#xff09;&#xff1a;保留课堂笔记中可运行的代码部分&#xff0c;课后debug跑一跑。学习语言初期应该多写代码&#xff08;从仿写到自己写&#…...

【书生大模型实战】L2-LMDeploy 量化部署实践闯关任务

一、关卡任务 基础任务&#xff08;完成此任务即完成闯关&#xff09; 使用结合W4A16量化与kv cache量化的internlm2_5-7b-chat模型封装本地API并与大模型进行一次对话&#xff0c;作业截图需包括显存占用情况与大模型回复&#xff0c;参考4.1 API开发(优秀学员必做)使用Func…...

《编程学习笔记之道:构建知识宝库的秘诀》

在编程的浩瀚世界里&#xff0c;我们如同勇敢的探险家&#xff0c;不断追寻着知识的宝藏。而高效的笔记记录和整理方法&#xff0c;就像是我们手中的指南针&#xff0c;指引着我们在这片知识海洋中前行&#xff0c;不至于迷失方向。在这篇文章中&#xff0c;我们将深入探讨如何…...

DETR论文,基于transformer的目标检测网络 DETR:End-to-End Object Detection with Transformers

transformer的基本结构: encoder-decoder的基本流程为&#xff1a; 1&#xff09;对于输入&#xff0c;首先进行embedding操作&#xff0c;即将输入映射为向量的形式&#xff0c;包含两部分操作&#xff0c;第一部分是input embedding&#xff1a;例如&#xff0c;在NLP领域&…...

untiy有渲染线程和逻辑线程嘛

之前我也这么认为&#xff0c;其实unity引擎是单线程的&#xff0c;当然后续的jobs不在考虑范围内 如果你在一个awake 或者 start方法中 延时&#xff0c;是会卡住主线程的 比如 其实游戏引擎有一个基础简单理解&#xff0c;那就是不断的进行一个循环&#xff0c;在这个周期循…...

什么是数据仓库ODS层?为什么需要ODS层?

在大数据时代&#xff0c;数据仓库的重要性不言而喻。它不仅是企业数据存储与管理的核心&#xff0c;更是数据分析与决策支持的重要基础。而在数据仓库的各个层次中&#xff0c;ODS层&#xff08;Operational Data Store&#xff0c;操作型数据存储&#xff09;作为关键一环&am…...

permutation sequence(

60. Permutation Sequence class Solution:def getPermutation(self, n: int, k: int) -> str:def rec(k, l, ans, n):if(n0): return# 保留第一个位置&#xff0c;剩下数字的组合leftCom math.factorial(n - 1) #用于计算 (n-1) 的阶乘值ele k // leftCommod k % leftCo…...

PCL 三线性插值

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 三线性插值是一种在三维空间中使用已知数据点进行插值的方法。它是在立方体内的插值方法,通过利用立方体的八个顶点的已知值来估算立方体内任意一点的值。三线性插值扩展了一维的线性插值和二维的双线性插值。其基…...

JVM虚拟机(一)介绍、JVM内存模型、JAVA内存模型,堆区、虚拟机栈、本地方法栈、方法区、常量池

目录 学习JVM有什么用、为什么要学JVM&#xff1f; JVM是什么呢&#xff1f; 优点一&#xff1a;一次编写&#xff0c;到处运行。&#xff08;Write Once, Run Anywhere&#xff0c;WORA&#xff09; 优点二&#xff1a;自动内存管理&#xff0c;垃圾回收机制。 优点三&am…...

Python利用xlrd复制一个Excel中的sheet保留原格式创建一个副本(注:xlrd只能读取xls)

目录 专栏导读库的介绍库的安装完整代码总结 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的双手 &#x1f3f3;️‍&#x1f308; 博客主页&#xff1a;请点击——> 一晌小贪欢的博客主页求关注 &#x1f44d; 该系列文…...

40、Python之面向对象:扩展的对象属性解析顺序(描述符 + MRO)

引言 在上一篇文章中&#xff0c;我们简单回顾了Python中在继承语境下的属性解析顺序&#xff0c;同时补充了能够控制、影响属性解析的3个函数/方法&#xff08;2个魔术方法 1个内置函数&#xff09;&#xff0c;相信对Python中属性的解析&#xff0c;相较于MRO&#xff0c;有…...

怎么制作网站栏目页主页/seo可以从哪些方面优化

在上一篇文章《欲练此功必先自宫之STM32汇编启动&#xff0c;放慢是为了更好的前行》中我们对STM32启动前的汇编部分的代码进行了分析。今天来一篇硬件设计方面的文章&#xff0c;教大家如何安装Altium Designer18。目前最新的Altium Designer最新版已经到了Altium Designer20&…...

常用网站开发软件/网络推广平台网站推广

目录&#xff1a;一、变量 二、变量类型 三、条件判断 四、循环 五、函数 六、模块 七、数据结构一、变量变量用来存放数据&#xff0c;语法&#xff1a;变量名 变量值&#xff0c;一般为了便于阅读&#xff0c;变量名采用数据意义数据类型来命名。如namestr 马云&#xff0c;…...

如何在人力资源网站做合同续签/网站统计分析工具

在注重实效的途径中&#xff0c;为我们介绍了一些原则。 首先是重复的危害。其中有一句关键&#xff0c;系统中的每一项知识都必须具有单一&#xff0c;无歧义&#xff0c;权威的表示。——不要重复你自己。有些重复是强加的&#xff0c;比如说建立具有重复信息的文档&#xff…...

河北建设集团网站/软件制作

1. 问题描述 使用 Iris 数据集&#xff0c;在一个 figure 中绘制出右侧的 16 个子图。 分别使用花瓣长度、花瓣宽度、花萼长度和花萼宽度这四种数据&#xff0c;两两组合&#xff0c;形成散点。 找一组自己感兴趣的真实数据&#xff0c;绘制出饼图。并看看数据的项数在什么范围…...

怎么给网站添加统计代码/写文章一篇30元兼职

记录当时入职CDG的感想 我主要负责内部运营平台的系统测试工作&#xff0c;刚入职&#xff0c;老大先给了我一个运营中心项目迭代流程文档&#xff0c;让我熟悉熟悉内部运营平台。我一看&#xff0c;啊哈&#xff0c;作为软件工程的学生&#xff0c;敏捷开发、双周迭代还是有那…...

wordpress帮助手册/常用的搜索引擎有哪些?

前言 本文主要记录下关于斯坦福CS231n课程Lecture1——Lecture5中学习的笔记&#xff0c;以下部分内容为个人理解如有错误&#xff0c;敬请原谅。 一、传统机器学习和深度学习联系 不管是传统的机器学习还是深度学习&#xff0c;贯穿主线的就是特征&#xff0c;只不过传统的机…...