鸿蒙(API 12 Beta3版)【使用投播组件】案例应用
华为视频接入播控中心和投播能力概述**
华为视频在进入影片详情页播放时,支持在控制中心查看当前播放的视频信息,并进行快进、快退、拖动进度、播放暂停、下一集、调节音量等操作,方便用户通过控制中心来操作当前播放的视频。
当用户希望通过大屏播放当前华为视频的影片时,可以在华为视频或播控中心内进行投播,将影片投播到同一网络下的华为智慧屏等大屏设备进行播放,且通过播控中心来方便地进行播放暂停、快进快退、下一集等操作。
华为视频投播功能需要使用播控中心的能力完成,所以在接入投屏之前,华为视频需要先接入播控中心。
华为视频接入播控中心
华为视频接入播控中心介绍
- 媒体会话(AVSession):本地播放时用于更新媒体资源的信息和响应系统播控中心,接入参考[媒体会话提供方]。在投播时,AVSession作为在本地播放和投播之间切换的“枢纽”接口,把二者联系起来。通过AVSession可以设置和查询应用投播能力,并创建投播控制器。
- 媒体会话控制器(AVSessionController):一般由播控中心提供。如果是应用内的控制器,可用于控制应用的后台播放。
华为视频接入播控中心的交互流程如图所示。
华为视频同步播控中心
-
播放内容信息上报播控中心
这部分功能负责实现在应用播放的时候,通知播控中心当前播放的影片信息。
- 应用冷启动之后,需要调用[createAVSession]创建会话。应用生命周期结束后会话自动销毁,不需要调用destroy。
说明
下文中代码示例,可能包含重复的函数和导包引入,因此后续代码示例不再重复展示。
- 导入相关模块
// 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';
- 调用[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 从华为视频内播放到投播成功
实现投播效果需要完成以下步骤。
- 使用隔空投放组件连接远端设备
用户在播放影片时,右上角会展示一个
图标,它提供了[投播]能力。用户点击该图标后,播控中心将拉起设备选择的模态窗口,设备的搜索发现、用户选择设备后的连接均由播控中心完成,此过程华为视频不感知。完成连接后,播控中心通过播放设备变化的监听事件outputDeviceChange通知华为视频,华为视频再进行下一步处理。
图2 点击投播组件触发设备选择弹框
应用使用[AVSession.on(‘outputDeviceChange’)]设置播放设备变化的监听事件,示例代码如下。
远端设备能够投播,需要满足以下条件:
-
设备连接成功,即outputDeviceChange事件监听回调返回connectState为1。
-
OutputDeviceInfo中设备列表的第一个设备,必须为远端设备,即castCategory为CATEGORY_REMOTE。
-
投播协议类型必须支持Cast+ Stream。
- 导入相关模块
// 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';
- 设置播放设备变化的监听事件示例代码:
// 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 应用播放框内投播选择清晰度
// 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状态
以下是具体的实现样例代码:
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 远端设备播放器
样例代码如下:
// 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 播控中心发起投播的流程
具体的实现原理如下:
-
用户选择设备且连接成功之后,播控中心触发on(‘outputDeviceChange’)回调通知应用,应用可感知到有投播设备正在试图连接。
-
后续流程与应用发起投播流程一致。可参考[华为视频应用内发起投播],大致分为:
- 根据ConnectionState判断是否连接成功。
- 根据DeviceInfo.castCategory判断是否远程设备。
- 根据DeviceInfo.supportedProtocols判断投播协议类型必须支持Cast+ Stream。
- 上述步骤均判断成功后,让应用请求投播播放URL,解析地址,投送URL。
连续播放影片的投播流程
对于连续播放,当前的方案如图所示。
图7 切换不同影片时播控中心显示
- 如果应用仅在本地播放,播放状态上报给播控中心。(应用本地播放影片1)
- 如果应用进行投播播放,则将投播状态上报到播控中心。(应用投播影片1)
- 投播过程中,应用如果进行其他影片的本地播放,不会通知播控中心。(应用本地播放影片2,此时播控中心仍然显示影片1)
- 当在投播过程中,本地播放的内容如果想触发投播时,需要通过播控中心提供的投播按钮实现。播控中心提供投播按钮的图片资源,应用内置做成按钮。播放框内点击这个按钮,直接将新内容的MetaData和投播URL都替换当前投播内容的方式实现。即对于播控中心,仅认为是投播中的内容变化了。
- 投播和本地播放并存时,投播如果突然断开,当前播放的本地内容将立刻上报到播控中心。
播控状态显示和操作指令
-
华为视频应用
- 状态显示
图8 华为视频app内状态显示
通过监听on(type: ‘playbackStateChange’)事件,获取播放状态。
AVPlaybackState属性定义请参考[API文档]。
// 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 切换影片不同剧集时播控中心显示
影片播放结束以后,一般来说,电影是没有下一集,而电视剧是有下一集的,处理不同场景的处理方法如下:
对于切换不同电影、电视剧的场景
此时应该结束投播,华为视频应用内从刚刚播放的影片继续播放,远端播放器结束播放。
对于切换剧集(同一电视剧不同集或预告、花絮等)的场景
为了方便用户,此时不应该结束投播,应获取到下一集的播放URL,继续自动投播下一集。具体的实现如下:
- 判断是否可以投播下一集:当前影片有下一集且华为视频应用感知到当前影片即将播放完毕,满足上述条件,即可投播下一集。
说明
华为视频app投播时判断影片即将播放完毕有两种方式:
- 收到avCastController的endOfStream回调时。
- 判断播放进度是否到了最后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');});}
}
- 清晰度切换
- 开始投播的时候,多个清晰度的二级URL都已经解析好了。
用户选择清晰度后,就找到对应清晰度的二级URL然后调用prepare和start接口进行投播。
- 播放进度:按照当前触发切换时候的进度点开始继续播放。
样例代码:
// 业务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 播放鉴权报错时的错误提示
样例代码:
// 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 播控中心状态显示
- 操作路径
从系统顶部下拉进入控制中心 -> 点击影片海报进入播控中心页面 -> 点击右上角投播按钮进入投音控制页面;
从系统顶部下拉进入控制中心 -> 点击右上角投播按钮进入投音控制页面。
- 状态显示
支持展示影片的标题、副标题、海报、播放状态、当前进度、总时长等信息。
- 操作指令
支持播放暂停、进度SEEK、下一集、快进快退、调节SINK端音量、退出投播、切换设备等操作。
对以下场景,华为视频进行了监听处理:
-
播放暂停、进度SEEK、快进快退等状态监听,即监听playbackStateChange事件,详细代码可参考[华为视频应用-状态显示]。
-
下一集。
监听到下一集事件时,通知详情页组件播放下一集。此时会触发自动投播的流程,具体可参考[操作指令-自动下一集]。
- 播放结束。
播放结束后,和下一集的操作类似,可以选择结束投播,具体可以参考[操作指令-结束投播]。
- 退出投播、切换设备。
退出投播,其实就是切换到本机播放;切换设备,也是先切换到本机,再投播到其他设备,流程是类似的,都是监听outputDeviceChange事件,处理代码可以参考[使用隔空投放组件连接远端设备]。
- 远端设备播放器
远端设备播放器应用状态显示和操作指令,其UI界面如下:
图12 大屏播放器播放状态显示
- 状态显示
支持展示影片的标题、副标题、播放状态、当前进度、总时长等信息。
- 操作指令
支持播放暂停、进度SEEK、下一集、快进快退、退出投播(关闭播放器)、设置播放速度等操作。
对播放暂停、进度SEEK、下一集、快进快退、退出投播场景,华为视频进行了监听处理,具体实现可以参考[华为视频应用-操作指令]。
本地视频支持投播
图13 本地视频投播
本地视频投播的逻辑和在线视频基本一致,具体差异点如下所示:
- 不支持下一集、自动下一集。
- 不支持清晰度切换。
- 调用prepare接口时,不传mediaUri,传fdSrc(本地文件句柄media.AVFileDescriptor类型)。
完整示例代码
注意
代码目录结构如下所示,请开发者参考代码时注意文件的路径。
// 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版)【使用投播组件】案例应用
华为视频接入播控中心和投播能力概述** 华为视频在进入影片详情页播放时,支持在控制中心查看当前播放的视频信息,并进行快进、快退、拖动进度、播放暂停、下一集、调节音量等操作,方便用户通过控制中心来操作当前播放的视频。 当用户希望通…...

【STM32项目】在FreeRtos背景下的实战项目的实现过程(一)
个人主页~ 这篇文章是我亲身经历的,在做完一个项目之后总结的经验,虽然我没有将整个项目给放出来,因为这项目确实也是花了米让导师指导的,但是这个过程对于STM32的实战项目开发都是非常好用的,可以说按照这个过程&…...
C#垃圾处理机制相关笔记
C#编程中的垃圾处理机制主要通过垃圾回收器(Garbage Collector,GC)实现自动内存管理。C#作为一种托管语言,其垃圾处理机制显著减轻了程序员的内存管理负担,与C语言等非托管语言形成鲜明对比。具体介绍如下:…...
C语言memcmp函数
目录 开头1.什么是memcmp函数?2.memcmp函数的内部程序流程图 3.memcmp函数的实际应用比较整型数组比较短整型二维数组比较结构体变量…… 结尾 开头 大家好,我叫这是我58。今天,我们要学一下关于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(Role-Based Access Control)模型的权限管理系统和字典数据管理模块,后端则使用了Spring Boot框架࿰…...
详细介绍Pytorch中torchvision的相关使用
torchvision 是 PyTorch 的一个官方库,主要用于处理计算机视觉任务。提供了许多常用的数据集、模型架构、图像转换等功能,使得计算机视觉任务的开发变得更加高效和便捷。以下是对 torchvision 主要功能的详细介绍: 1. 数据集(Dat…...
AI部署——主流模型推理部署框架
我们以最经典的Yolov5目标检测网络为例解释一下10种主流推理部署框架的大概内容,省略模型训练的过程,只讨论模型转换、环境配置、推理部署等步骤。 Intel的OpenVINO — CPUNvidia的TensorRT — GPU/CPUOpenCV DNN Module — GPU/CPUMicrosoft ONNX Runti…...

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

Vscode——如何实现 Ctrl+鼠标左键 跳转函数内部的方法
一、对于Python代码 安装python插件即可实现 二、对于C/C代码 安装C/C插件即可实现...
力扣热题100_回溯_78_子集
文章目录 题目链接解题思路解题代码 题目链接 78. 子集 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1: 输入ÿ…...
浏览器如何工作(一)进程架构
分享cosine 大佬,版权©️大佬所有 浏览器的核心功能 浏览器,“浏览” 是这个产品的核心,浏览无非分为两步: 获取想浏览的资源 展示得到的资源 现代浏览器还增加了交互功能,这涉及到脚本运行。因此,…...
【LeetCode】两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。 示例 1…...

UE5学习笔记11-为拿取武器添加动画
一、一点说明 动画实例通过扩展为所有机器上的每个字符都存在动画蓝图,动画实例只能访问该计算机上的变量。 二、思路 我在武器组件中有一个武器类的指针,判断当前指针是否为空去判断当前角色是否装备武器 三、实现 1.在角色C类中添加是否装备武器的函…...
68. 文本左右对齐【 力扣(LeetCode) 】
一、题目描述 给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单…...

【中等】 猿人学web第一届 第6题 js混淆-回溯
文章目录 请求流程请求参数 加密参数定位r() 方法z() 方法 加密参数还原JJENCOde js代码加密环境检测_n("jsencrypt")12345 计算全部中奖的总金额请求代码注意 请求流程 请求参数 打开 调试工具,查看数据接口 https://match.yuanrenxue.cn/api/match/6 请…...
低、中、高频率段具体在不同应用中的范围是多少
1、低频率段(Low Frequency Range) ①建筑声学和噪声控制:通常将20 Hz 到 200 Hz 的频率范围视为低频段。在这一范围内,声音的波长较长,通常与低音(如重低音音乐)和建筑结构中的振动有关。 ②…...

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…...

计算机学生高效记录并整理编程学习笔记的方法
哪些知识点需要做笔记? 以下是我认为计算机学生大学四年可以积累的笔记。 ① 编程语言类(C语言CJava):保留课堂笔记中可运行的代码部分,课后debug跑一跑。学习语言初期应该多写代码(从仿写到自己写&#…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
是不是受够了安装了oracle database之后sqlplus的简陋,无法删除无法上下翻页的苦恼。 可以安装readline和rlwrap插件的话,配置.bahs_profile后也能解决上下翻页这些,但是很多生产环境无法安装rpm包。 oracle提供了sqlcl免费许可,…...
6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础
第三周 Day 3 🎯 今日目标 理解类(class)和对象(object)的关系学会定义类的属性、方法和构造函数(init)掌握对象的创建与使用初识封装、继承和多态的基本概念(预告) &a…...

2.3 物理层设备
在这个视频中,我们要学习工作在物理层的两种网络设备,分别是中继器和集线器。首先来看中继器。在计算机网络中两个节点之间,需要通过物理传输媒体或者说物理传输介质进行连接。像同轴电缆、双绞线就是典型的传输介质,假设A节点要给…...