# AVPlayer **Repository Path**: openharmonie-e/avplayer ## Basic Information - **Project Name**: AVPlayer - **Description**: 简单的视频播放器一枚~ - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2024-07-30 - **Last Updated**: 2025-05-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: 视频播放器 ## README # 第八章---网络视频播放器 本章主要介绍网络视频流播放开发的相关知识和所需技术,通过对这一章节的学习能够掌握视频播放的流程、emitter事件推送及响应的方法以及一定的代码封装知识,入门媒体开发的领域,补全对于媒体服务板块了解的缺失,为以后进行更进一步的媒体服务知识运用奠定基础。 **本章主要内容** + 视频播放流程 + AVPlayer(媒体资源播放管理类)接口的介绍和使用 + emitter事件管理机制的使用 + 手势控制的开发 + 代码封装的技巧 ## 8.1 案例功能及知识点总览 ### 8.1.1 案例功能结构图 ![whiteboard_exported_image (6)](./README.assets/whiteboard_exported_image%20(6).png) ### 8.1.2 案例知识点总览以及核心概念 ![whiteboard_exported_image (7)](./README.assets/whiteboard_exported_image%20(7).png) #### 视频格式介绍 常见的视频格式有非常多种,例如MP4、WebM、AVI、MKV、MOV、WMV等。而鸿蒙主要支持mp4、mpeg-ts、mkv格式的视频播放,以下是对上述三种格式的基本介绍。 1. **MP4 (MPEG-4 Part 14)** 1. 后缀:`.mp4` 2. MP4是一种广泛使用的数字多媒体格式,属于MPEG-4标准的一部分。 3. 特点: + 压缩率高:使用先进的视频压缩算法(如H.264、H.265),在保证视频质量的同时,显著减少文件大小。 + 多媒体容器:不仅可以存储视频和音频,还可以包含字幕、静态图片和其他数据。 + 广泛支持:几乎所有的媒体播放器和设备都支持MP4格式,包括PC、智能手机、平板电脑和网络浏览器。 4. 缺点:专利限制,部分编解码器使用可能需要支付专利费用。 2. **MPEG-TS (Transport Stream)**: 1. 后缀:`.ts` 2. MPEG-TS是一种专门用于传输和存储视频、音频和数据的容器格式,特别适用于流媒体传输。 3. 特点: + 实时传输优化:专为实时传输和流媒体设计,能够容错和同步音视频流,适用于直播和广播。 + 分割传输:将数据分割成小的数据包,适合不可靠的网络环境(如卫星、广播)。 + 多路复用:支持多种节目和流的同时传输。 4. 缺点: + 文件相对较大,不适合存储和点播。 + 播放器支持相对有限,主要用于专业设备。 3. **MKV** **(Matroska Video)** 1. 后缀:`.mkv` 2. MKV是一种开源的多媒体容器格式,广泛应用于高清和超高清的视频内容。 3. 特点: + 开源和免费:无专利限制,任何人都可以自由使用和修改。 + 高度可定制:支持多种视频、音频、字幕编解码器和格式。 + 多媒体支持:能够包含多个视频、音频、字幕流以及元数据。 4. 缺点: + 部分旧设备和播放器可能不支持。 + 文件较大,不如MP4和其他格式压缩率高。 对于网络视频而言,直播流媒体是一种更为常见的使用手段。其中hls以其适应性强和广泛兼容性的优势,成为使用最为广泛的流媒体传输协议之一。 + **HLS** **(HTTP Live Streaming)** + 后缀:`.m3u8` + HLS是一种流媒体传输协议,常用于直播或按需视频流媒体传输。`.m3u8`文件是一个索引文件,包含多个`.ts`(MPEG-2 Transport Stream)视频片段的列表。 + 特点: + 原生支持iOS和macOS设备,并且现代浏览器和媒体播放器也广泛支持HLS,确保在多种设备和平台上流畅播放。 + 将视频切分成小段(一般为几秒钟),每段独立传输,即使在不稳定的网络环境下也能保证流畅播放。如果某一段视频数据丢失或损坏,播放器可以跳过该段继续播放,不影响整体观看体验。 + 缺点:HLS通常有较高的延迟(几秒到几十秒),对于实时互动和低延迟应用(如在线游戏、视频会议等)来说不够理想。 以上知识仅作了解即可,对于整体构建媒体服务体系有帮助,在未来对于视频录制的开发中会有所体现和使用,而对于视频播放开发而言无需过多关注和在意这部分内容带来的影响。 #### 网络视频路径 在鸿蒙的音视频播放开发中,有一个最为关键的类——AVPlayer,基本上所有的操作都与这个类的属性和方法挂钩。AVPlayer是一个播放管理类,用于管理和播放媒体资源。在调用AVPlayer的方法前,需要先通过`createAVPlayer()`构建一个AVPlayer实例。 AVPlayer中的url属性与需要播放的视频资源绑定,其支持的路径包含文件路径(fd://xx)以及三种网络路径,分别是http、https、hls。对于单单获取网络视频资源而言,http和https的区别开发者无需在意,而需要关注和区分的是,http/https索引的视频资源以常见的视频格式作结,例如`https://example.com/test.mp4`,而hls类型的视频资源常以.m3u8作结,例如`https://example.com/test.m3u8`。 #### 视频播放流程 ![whiteboard_exported_image (8)](./README.assets/whiteboard_exported_image%20(8).png) 了解视频播放的流程其实本质上是了解视频播放过程中能够处于的各个状态。在开发所必需使用的api中(里面包括了AVPlayer类),定义了AVPlayerState类型,被此类型定义的变量只能存储仅有的几个状态值字面量。AVPlayerState包含了视频播放过程中所有可能处于的状态,所以AVPlayerState又称为AVPlayer的状态机。以下是对于AVPlayerState的描述: | **名称** | **类型** | **说明** | | ----------- | -------- | ------------------------------------------------------------ | | idle | string | 闲置状态。来源:createAVPlayer(),AVPlayer类对象所有属性值为默认。[initialized/prepared/playing/paused/completed/stopped/error状态下]对象.reset(),对象中设置的url属性、loop属性被重置成默认。去向:"initialized":设置对象的url属性进入。"released":调用对象.release()方法。 | | initialized | string | 初始化状态。来源:设置url可执行操作:设置surfaceId属性,配置视频播放的窗口,作为唯一标识与XComponent组件对应。去向:"prepared":调用对象.prepare()方法。"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | | prepared | string | 已准备状态。来源:[initialized状态下]对象.prepare(),播放引擎(与单一AVPlayer对象关联)中的资源准备就绪。[stopped状态下]对象.prepare(),重新加载播放引擎中的内存资源。去向:"palying":调用对象.paly()方法。"stopped":调用对象.stop()方法。"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | | playing | string | 正在播放状态。来源:[prepared/paused/completed状态下]对象.play(),视频开始播放。可执行操作:调用对象.seek()方法,跳转到指定的时间节点上。去向:"paused":调用对象.pause()方法。"completed":当前视频播放到结尾时。"stopped":调用对象.stop()方法。"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | | paused | string | 暂停状态。来源:[playing状态下]对象.pause(),暂停当前视频的播放。去向:"palying":调用对象.paly()方法。"stopped":调用对象.stop()方法。"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | | completed | string | 播放至结尾状态。来源:[playing状态下]当媒体资源播放至结尾时,如果用户未设置循环播放(loop = true),AVPlayer对象会进入completed状态。去向:"palying":调用对象.paly()方法。"stopped":调用对象.stop()方法。"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | | stopped | string | 停止状态。来源:[prepared/playing/paused/completed状态下]对象.stop(),播放引擎只保留属性,但会释放内存资源。去向:"prepared":调用对象.prepare()方法。"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | | released | string | 销毁状态。 来源:[idle/initialized/prepared/playing/paused/completed/stopped/error状态下]对象.release(),销毁与当前AVPlayer关联的播放引擎,无法再进行状态转换,结束流程。注意:AVPlayer对象还存在,并不会变成null,但无与之关联的播放引擎,已经不能进行任何操作,在作用上将其当作是null。只能通过创建一个新的AVPlayer对象来重新关联。去向:无。重新createAVPlayer()。 | | error | string | 错误状态。来源:播放引擎发生不可逆的错误。去向:"idle":调用对象.reset()方法。"released":调用对象.release()方法。 | 也就是说,AVPlayerState类型的变量只能是九个状态值之一,本质为string类型。 #### AVPlayer类介绍 存放在@ohos.multimedia.media (媒体服务)模块中,使用前需要先导入模块: ```TypeScript import media from '@ohos.multimedia.media'; ``` AVPlayer是播放管理类,用于管理和播放媒体资源,主要是音频和视频。 使用时是使用AVPlayer的对象。虽然是一个类,但不能通过new方法来进行创建,需要使用特定的函数createAVPlayer()来构建一个AVPlayer实例(实例就是对象),确保创建后此实例能与播放引擎关联。 AVPlayer包含众多的属性和方法,在这里仅罗列本案例使用到的部分属性方法,具体的介绍在代码实战部分穿插讲解。若想了解更多,请前往官方文档处查阅。 https://docs.openharmony.cn/pages/v4.1/zh-cn/application-dev/reference/apis-media-kit/js-apis-media.md#avplayer9 **属性** | **名称** | **类型** | **可读** | **可写** | **简单说明** | | ----------- | ------------- | -------- | -------- | ------------------------------------------------------------ | | url | string | √ | √ | 设置媒体URL,只允许在**idle**状态下设置。 | | surfaceId | String | √ | √ | 设置视频窗口ID,默认无窗口,只允许在**initialized**状态下设置。 | | state | AVPlayerState | √ | × | 查询音视频播放的状态。 | | currentTime | number | √ | × | 查询视频的当前播放位置,单位为毫秒(ms)。 | | duration | number | √ | × | 查询视频时长,单位为毫秒(ms)。 | **事件注册** | **名称** | **简单说明** | | ----------------- | ---------------------------------------- | | on('stateChange') | 监听播放状态机AVPlayerState切换的事件。 | | on('error') | 监听AVPlayer的错误事件,仅用于错误提示。 | | on('seekDone') | 监听seek生效的事件。 | | on('endOfStream') | 监听资源播放至结尾的事件。 | | on('timeUpdate') | 监听资源播放当前时间,单位为毫秒(ms)。 | **方法** | **名称** | **简单说明** | | -------- | -------------------- | | prepare | 准备播放音频/视频。 | | play | 开始播放音视频资源。 | | pause | 暂停播放音视频资源。 | | stop | 停止播放音视频资源。 | | reset | 重置播放。 | | release | 销毁播放资源。 | | seek | 跳转到指定播放位置。 | ## 8.2 开发准备工作 + 系统版本:OpenHarmony 4.0 Release + SDK版本:API 10 + Model类型:Stage + DevEco Studio版本:DevEco Studio 4.0 release + 硬件环境:RK3588 > 设置网络播放路径,即使用网络地址为AVPlayer类的url属性赋值时,需声明权限`ohos.permisson.INTERNET`才能正确获取到,进入initialized状态。 ### 权限配置 在模块级别的module.json5配置文件下添加`requestPermissions`标签,具体路径为**entry > src > main > module.json5**。`requestPermissions`的值是一个对象数组,标识当前应用运行时需向系统申请的权限集合。 此对象类型包含name、reason和usedScene三个属性,对本案例需要配置的权限而言只需要关注name属性即可。name为字符串类型,标识需要使用的权限名称,不可缺省。配置如下: ```JSON // module.json5 { "module": { ... "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } } ``` ## 8.3 代码实战部分 ### 8.3.1 代码结构解读 ```Plain ├──entry/src/main/ets // 代码区 │ ├──entryability │ │ └──EntryAbility.ets // 程序入口类 │ ├──model │ │ └──MediaPlayer.ets // 媒体播放接口 │ ├──pages │ │ ├──GetFromFiles.ets // 文件获取视频播放页(留待学习者结合开发) │ │ ├──GetFromInternet.ets // 网络获取视频播放页 │ │ └──Home.ets // 应用主页面 │ ├──utils │ │ ├──constants │ │ │ ├──Constants.ets // 数据常量封装 │ │ │ └──UIStyle.ets // UI常量封装 │ │ ├──data │ │ │ └──DevButtonData.ets // 开发者模式控制台参数数据 │ │ ├──log │ │ │ └──Logger.ets // 日志类 │ │ ├──struct │ │ │ ├──ActSelect.ets // 选中行为类(用以包装以实现引用传递) │ │ │ └──DeveloperOpe.ets // 开发者模式控制台参数类 │ │ └──tool │ │ └──TimeUtils.ets // 格式化时间类 │ └──view │ ├──ContentFrame.ets // 下拉框内容组件 │ ├──DropDownInputBox.ets // 下拉框组件 │ ├──HomeModule.ets // 功能模块组件 │ ├──Operator.ets // 控制台组件 │ ├──PlayControl.ets // 视频播控组件 │ ├──PlayWindow.ets // 视频窗口组件 │ ├──Return.ets // 上一页返回组件 │ └──SelectMode.ets // 模式选择组件 └──entry/src/main/resources // 资源文件目录 ``` ### **8.3.2 常量定义** 常量放置在一个文件下,避免直接暴露在代码中,不仅便于统一修改和维护,用常量名代替硬编码的值提高代码的可读性,也能够使得常量可以在代码库中被隐藏,同时一些常用的配置或参数在多个项目或模块中重用,减少重复代码的编写。 常量文件的编写常常和开发任务完成后的封装息息相关,按照开发顺序而言理应放在最后再来细谈,但这里提前说明是因为介绍各项功能时所展示的源码中包含大量的自定义常量,放在开头更有利于读者去理解源码的内容和逻辑,而不必要纠结于常量所代表的意义。在就本文档复现代码案例的时候,也可直接先定义好常量文件,从而实际开发中免于对尺寸、颜色等样式的调试。 依据视图与业务分离的原则,常量定义应该区分UI数据和业务使用数据。 #### **8.3.2.1 数据常量封装** 1. 创建一个默认导出的常量类,里面所有的常量资源以静态方式定义,表示常量属性直接与类本身进行挂钩。 ```TypeScript // Constants.ets export default class Constants { ... } ``` 2. 通知UI刷新所需常量 ```TypeScript // Constants.ets import emitter from '@ohos.events.emitter'; export default class Constants { /** * InnerEvent:通知UI更新 * UPDATE_PROGRESS_BAR_EVENT:进度条更新 */ static UPDATE_PROGRESS_BAR = 0x55; static UPDATE_PROGRESS_BAR_EVENT: emitter.InnerEvent = { eventId: Constants.UPDATE_PROGRESS_BAR, priority: emitter.EventPriority.IMMEDIATE }; /** * 播放状态监听:播放成功,暂停成功,完成 */ static PLAYER_PLAY_SUCCESS = 0x0001; static PLAYER_PAUSE_SUCCESS = 0x0002; static PLAYER_COMPLETE_SUCCESS = 0x0003; /** * 当前播放器状态: 正在播放,已暂停,已终止,空 */ static IS_PLAYING = 0x0004; static IS_PAUSED = 0x0005; static IS_STOPPED = 0x0006; static IS_IDLE = 0x0007; /** * InnerEvent:通知UI更新 * PLAYER_PLAY_SUCCESS_EVENT:播放成功,通知UI更新播放键 * PLAYER_PAUSE_SUCCESS_EVENT:暂停成功,通知UI更新播放键 * PLAYER_COMPLETE_SUCCESS_EVENT:播放完成,通知UI可以进行下一步操作 */ static PLAYER_PLAY_SUCCESS_EVENT: emitter.InnerEvent = { eventId: Constants.PLAYER_PLAY_SUCCESS, priority: emitter.EventPriority.IMMEDIATE }; static PLAYER_PAUSE_SUCCESS_EVENT: emitter.InnerEvent = { eventId: Constants.PLAYER_PAUSE_SUCCESS, priority: emitter.EventPriority.IMMEDIATE }; static PLAYER_COMPLETE_SUCCESS_EVENT: emitter.InnerEvent = { eventId: Constants.PLAYER_COMPLETE_SUCCESS, priority: emitter.EventPriority.IMMEDIATE }; ... } ``` 3. 数字常量 ```TypeScript // Constants.ets export default class Constants { ... /** * 常量number资源 */ static readonly INIT_NUM = 0; static readonly DURATION = 1500; static readonly PERCENTAGE_MULTI = 100; static readonly PERCENTAGE_DIV = 0.01; static readonly CLICK_RESPOND = 1; static readonly RESIDENCE_TIME = 2000; static readonly TRANSIENT_DISPLAY_TIME = 3000; ... } ``` 4. 图标常量 ```TypeScript // Constants.ets export default class Constants { ... /** * ICON资源 */ static RETURN_BACK_ICON = $r("app.media.return"); static PULL_DOWN_ICON = $r("app.media.pull_down"); static FOLDER_ICON = $r("app.media.folder"); static INTERNET_ICON = $r("app.media.Internet"); static PLAYER_PLAY_ICON = $r('app.media.play'); static PLAYER_PAUSE_ICON = $r('app.media.pause'); static PLAYER_BACKWARD_QUICK_ICON = $r("app.media.backwardSquare"); static PLAYER_FORWARD_QUICK_ICON = $r("app.media.forwardSquare"); ... } ``` 5. 字符串常量 ```TypeScript // Constants.ets export default class Constants { ... /** * string资源 */ // Home static readonly WELCOME_SPEECH = "欢迎使用简易视频播放器~"; static readonly GET_FROM_FOLDER = "从文件获取"; static readonly GET_FROM_INTERNET = "从网络获取"; static readonly REJECT_JUMP = "不好意思嗷~~,这个功能还没弄好呢"; static readonly JUMP_PAGE = "pages/GetFromInternet"; static readonly JUMP_SUCCESS = "Succeeded in jumping to the GetFromInternet page."; // GetFromInternet static readonly MEDIA_PLAYER_IS_NULL = "MediaPlayer is not created! Pleased create the instance object first." static readonly USER_MODE = "用户使用"; static readonly DEVELOPER_MODE = "开发者调试"; static readonly SEPARATOR_SELECTOR = "/"; static readonly ONE_STEP_PLAY = "一键播放"; static readonly INITIALIZE = "初始化"; static readonly PREPARE = "加载准备"; static readonly PLAY = "开始播放"; static readonly PAUSE = "暂停视频"; static readonly STOP = "终止播放"; static readonly RESET = "重置资源"; static readonly RELEASE = "销毁实例"; static readonly URL_IS_WRONG = "请选择正确的网络地址配置好您的url"; static readonly PRE_PREPARE = "请稍等片刻"; static readonly TEXT_INPUT_PROMPT = "请输入网络视频链接"; static readonly GET_URL_BUTTON = "获取"; static readonly GET_URL_SUCCESS = "url获取成功"; static readonly GET_URL_FAIL = "未成功获取,请输入url地址"; static readonly PRESET_NETWORK_URL_1 = "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8"; static readonly PRESET_NETWORK_URL_2 = "https://media.w3.org/2010/05/sintel/trailer.mp4"; static readonly DEFAULT_STRING = ""; static readonly DEFAULT_TIME = "00:00"; static readonly NETWORK_LINK_TAG_1 = "http://"; static readonly NETWORK_LINK_TAG_2 = "https://"; static readonly VIDEO_COMPLETED = "视频播放结束啦"; static readonly X_COMPONENT_ID = "VideoPlayer"; static readonly X_COMPONENT_TYPE = "surface"; static readonly SEEKING_START = "PLAYER START SEEKING"; static readonly SEEKING_END = "PLAYER END SEEKING"; static readonly HINT_AFTER_RETENTION = "当前已释放与此avPlayer关联的播放引擎,拒绝任何操作,请重新创建实例对象"; // MediaPlayer static readonly CREATE_AVPLAYER_SUCCESS = "createAVPlayer success"; static readonly CREATE_AVPLAYER_FAIL = "createAVPlayer fail"; static readonly AVPLAYER_IS_NULL = "avPlayer is not created, not even once!"; static readonly AVPLAYER_STATE_IDLE = "idle"; static readonly AVPLAYER_STATE_INITIALIZED = "initialized"; static readonly AVPLAYER_STATE_PREPARED = "prepared"; static readonly AVPLAYER_STATE_PLAYING = "playing"; static readonly AVPLAYER_STATE_PAUSED = "paused"; static readonly AVPLAYER_STATE_COMPLETED = "completed"; static readonly AVPLAYER_STATE_STOPPED = "stopped"; static readonly AVPLAYER_STATE_RELEASED = "released"; static readonly AVPLAYER_STATE_ERROR = "error"; static readonly IDLE_CALLED = "AVPlayer state idle called."; static readonly INITIALIZED_CALLED = "AVPlayer state initialized called."; static readonly PREPARED_CALLED = "AVPlayer state prepared called."; static readonly PLAYING_CALLED = "AVPlayer state playing called."; static readonly PAUSED_CALLED = "AVPlayer state paused called."; static readonly COMPLETED_CALLED = "AVPlayer state completed called."; static readonly STOPPED_CALLED = "AVPlayer state stopped called."; static readonly RELEASED_CALLED = "AVPlayer state released called."; static readonly ERROR_CALLED = "Avplayer state error called."; static readonly SET_HINT = "注册对象实例的相关回调"; static readonly DEL_HINT = "取消对象实例的回调注册"; static readonly PREPARE_SUCCESS = "AVPlayer prepare succeeded."; static readonly PLAY_SUCCESS = "AVPlayer play succeeded."; static readonly PAUSE_SUCCESS = "AVPlayer pause succeeded."; static readonly STOP_SUCCESS = "AVPlayer stop succeeded."; static readonly RESET_SUCCESS = "AVPlayer reset succeeded."; static readonly RELEASE_SUCCESS = "AVPlayer release succeeded."; static readonly NONE_STATE = "undefined state"; } ``` #### 8.3.2.2 UI常量封装 定义常用值为独立变量,不同值但隶属同一属性则创建结构封装。不同值但统一类型使用枚举(enum)结构,不同值间存在不同类型使用类(class)结构。 1. 常用值独立定义 ```TypeScript // UIStyle.ets export const HEIGHT = "100%"; export const WIDTH = "100%"; export const BOTTOM = 20; export const MAX_LINE = 1; export const OPACITY = 0.6; export const SPACE = 10; export const GAP = 2; ``` 2. 枚举结构 ```TypeScript // UIStyle.ets export enum Color { FONT_WHITE = "#BBBBBB", GRADE1_BLACK = "#333840", GRADE2_BLACK = "#3D434D", GRADE3_BLACK = "#3C4147", SHADOW = "#ff2b2a2a", SELECTED_FUCHSIA = "#ff473d4d", PLACEHOLDER = "#54bbbbbb", PROGRESS = "#31bbbbbb", MODE_SELECTED = "#ff5e5066", MODE_UNSELECTED = "#2fbbbbbb", OPERATOR = "#ff313737" } export enum Radius { HOME_MODULE = 8, SHADOW = 10, INPUT_MODULE = 10, SELECT_MODULE = 12, WINDOW = 12, EXTERNAL = 5, INNER = 2 } export enum Param { ROTATE_X = 0, ROTATE_Y = 0, ROTATE_Z = 1, ROTATE_ANGLE_START = -90, ROTATE_ANGLE_END = 0 } export enum Weight { MODE = 1, OPERATOR = 4, TIER = 1 } export enum BorderWidth { USER = 2, DEVELOP = 1 } ``` 3. 类结构 ```TypeScript // UIStyle.ets // 包含不同类型的常量,使用class而不是enum export class Height { static readonly HOME_IMAGE = "25%"; static readonly HOME_GET = "30%"; static readonly HOME_MODULE = "80%"; static readonly HOME_CENTRAL_CONTAINER = "40%"; static readonly DROP_DOWN_CONTAINER = "30%"; static readonly INPUT_SUBJECT = "25%"; static readonly DROP_TAG = 20; static readonly PRESET_SELECTION = "50%"; static readonly CONTENT_SELECT = "45%"; static readonly RETURN_BACK = 25; static readonly SURFACE = 1080; static readonly WINDOW = "50%"; static readonly PLAY_CONTROL_CONTAINER = "30%"; static readonly ICON = "25%"; static readonly PROGRESS_MODULE = "20%"; static readonly PANEL = "35%"; } export class Width { static readonly HOME_IMAGE = "25%"; static readonly HOME_GET = "100%"; static readonly HOME_MODULE = "40%"; static readonly HOME_CENTRAL_CONTAINER = "60%"; static readonly DROP_DOWN_CONTAINER = "80%"; static readonly GET_URL_BUTTON = "20%"; static readonly INPUT_BOX = "60%"; static readonly DROP_TAG = 20; static readonly PRESET_SELECTION = "80%"; static readonly RETURN_BACK = 25; static readonly RETURN_CONTAINER = "95%"; static readonly SURFACE = 1920; static readonly WINDOW = "70%"; static readonly PLAY_CONTROL_CONTAINER = "70%"; static readonly ICON = "25%"; static readonly PROGRESS_BAR = "75%"; static readonly DIVIDER_CONTAINER = "90%"; static readonly DIVIDER = "40%"; static readonly MODE_CHANGE_MODULE = "90%"; static readonly PANEL = "85%"; static readonly OPERATOR = "70%"; static readonly TIER1_OPERATE_CONTAINER = "70%"; static readonly TIER2_OPERATE_CONTAINER = "95%"; } export class FontSize { static readonly HOME_GET = "40px"; static readonly GET_URL_BUTTON = 14; static readonly INPUT_CONTENT = 12; static readonly PLACEHOLDER = 12; static readonly CONTENT_SELECT = 13; static readonly TIME = 12; static readonly MODE_SELECTED = 16; static readonly MODE_UNSELECTED = 14; static readonly OPERATOR = 9; } export class Margin { static readonly HOME_IMAGE_TOP = "80px"; static readonly DROP_DOWN_CONTAINER_TOP = 15; static readonly PRESET_SELECTION_TOP = 28; static readonly CONTENT_DIVIDER_TOP = 1; static readonly CONTENT_DIVIDER_BOTTOM = 1; static readonly RETURN_CONTAINER_BOTTOM = 12; static readonly PROGRESS_BOTTOM = 2; } export class Padding { static readonly HOME_CENTRAL_CONTAINER = "10px"; static readonly DROP_DOWN_CONTAINER_LEFT = 10; static readonly DROP_DOWN_CONTAINER_RIGHT = 10; static readonly PRESET_SELECTION_RIGHT = 10; static readonly PRESET_SELECTION_LEFT = 10; static readonly PRESET_SELECTION_TOP = 2; static readonly PRESET_SELECTION_BOTTOM = 2; static readonly PANEL = 8; static readonly OPERATOR_TOP = 12; static readonly OPERATOR_BOTTOM = 12; } ``` ### 8.3.3 路由页面 ![1722325704134-7](./README.assets/1722325704134-7.jpeg) `Home.ets`文件作为程序的路由页面,也是点开应用后首先呈现的主页面,目前仅内置一个“从网络获取视频播放”的模块入口,点击模块即可跳转至演示当前模块功能的页面。 同时提供封装好的模块入口组件进行自定义的扩展,读者可根据第六章学习的应用文件管理知识,实现“从文件获取视频播放”的功能,根据第七章学习的使用相机进行录制的功能,集成上AVrecorder实现“视频录制并播放”的功能,感兴趣者还可以根据第八章对于AVPlayer的学习举一反三地实现“音频播放的功能”,从而搭建一套完整的媒体应用开发体系。 #### 8.3.3.1 主体组件 1. 导入相关模块 ```TypeScript // Home.ets import router from '@ohos.router'; import promptAction from '@ohos.promptAction'; import { BusinessError } from '@ohos.base'; import { BOTTOM, Color, HEIGHT, Height, Padding, WIDTH, Width } from '../utils/constants/UIStyle'; import Constants from '../utils/constants/Constants'; import HomeModule from '../view/HomeModule' ``` 2. 封装属性 ```TypeScript // Home.ets @Extend(Row) function container() { .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.SpaceEvenly) .padding(Padding.HOME_CENTRAL_CONTAINER) .size({ height: Height.HOME_CENTRAL_CONTAINER, width: Width.HOME_CENTRAL_CONTAINER }) } @Extend(Column) function root() { .backgroundColor(Color.GRADE1_BLACK) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .size({ height: HEIGHT, width: WIDTH }) } ``` 3. 组件内容 | **属性** | **方法** | | -------- | ---------------- | | 无 | jumpToInternet() | | | build() | ```TypeScript // Home.ets @Entry @Component struct Home { // jumpToFolder() { // try { // promptAction.showToast({ // message: Constants.REJECT_JUMP, // duration: Constants.DURATION, // bottom: BOTTOM // }); // } catch (error) { // let message = (error as BusinessError).message // let code = (error as BusinessError).code // console.error(`showToast args error code is ${code}, message is ${message}`); // }; // } jumpToInternet() { router.pushUrl({ url: Constants.JUMP_PAGE }).then(() => { console.info(Constants.JUMP_SUCCESS) }).catch((err: BusinessError) => { console.error(`Failed to jump to the GetFromInternet page.Code is ${err.code}, message is ${err.message}`); }) } build() { Column() { Text(Constants.WELCOME_SPEECH) .fontColor(Color.FONT_WHITE) Row() { // HomeModule({ // image: Constants.FOLDER_ICON, // text: Constants.GET_FROM_FOLDER, // onClickFunc: (): void => { this.jumpToFolder(); } // }) HomeModule({ image: Constants.INTERNET_ICON, text: Constants.GET_FROM_INTERNET, onClickFunc: (): void => { this.jumpToInternet(); } }) } .container() } .root() } } onClickFunc: (): void => { `**`this`**`.jumpToInternet(); } ``` > 使用箭头函数包装传递函数,是封装时一种良好的习惯。 > > 在想要传递的函数中,可能存在`this.`的调用,在这种情况下如果直接传递函数,会导致this指向不明确的问题,也就是说这个函数的上下文并没有被程序确定下来。而箭头函数使用的时候会自动捕获当前使用处的上下文,从而明确this的指向问题。 > > ts中原本是有解决这个问题的办法,就是使用Function.bind()——每个函数(在ts中每个函数也是一个变量,function是它们的类型)都会拥有的方法。但是在鸿蒙api10以上,语法检查更加严格,不允许使用各种涉及动态变换的操作,所以在api10环境下使用bind()会导致编译不成功。 #### 8.3.3.2 引用的自定义组件 使用了自定义组件`HomeModule`,以下是`HomeModule`的定义。 1. 导入相关模块 ```TypeScript // HomeModule.ets import Constants from '../utils/constants/Constants' import { Height, Margin, Width, Color, FontSize, Radius } from '../utils/constants/UIStyle' ``` 2. 封装属性 ```TypeScript // HomeModule.ets @Extend(Row) function image() { .size({ height: Height.HOME_IMAGE, width: Width.HOME_IMAGE }) .margin({ top: Margin.HOME_IMAGE_TOP }) } @Extend(Row) function text() { .width(Width.HOME_GET) .height(Height.HOME_GET) .backgroundColor(Color.GRADE3_BLACK) .justifyContent(FlexAlign.Center) .borderRadius(Radius.HOME_MODULE) } @Extend(Column) function module(onClick: () => void) { .justifyContent(FlexAlign.SpaceBetween) .backgroundColor(Color.GRADE2_BLACK) .size({ height: Height.HOME_MODULE, width: Width.HOME_MODULE }) .shadow({ radius: Radius.SHADOW, color: Color.SHADOW }) .borderRadius(Radius.HOME_MODULE) .onClick(onClick) } ``` 3. 组件内容 | **属性** | **方法** | | ----------------------------------- | -------- | | **private** image: ResourceStr | build() | | **private** text: string | | | **private** onClickFunc: () => void | | ```TypeScript // HomeModule.ets @Component export default struct HomeModule { private image: ResourceStr = Constants.DEFAULT_STRING; private text: string = Constants.DEFAULT_STRING; private onClickFunc: () => void = () => {}; build() { Column() { Row() { Image(this.image) .objectFit(ImageFit.Contain) } .image() Row() { Text(this.text) .fontColor(Color.FONT_WHITE) .fontSize(FontSize.HOME_GET) } .text() } .module(this.onClickFunc) } } ``` ### 8.3.4 视频播放业务类 将需要实现的业务能力全部封装至一个自定义的类中,其中包括初始化播放器(创建AVPlayer实例对象)、注册事件回调函数、相关事件推送以及对AVPlayer实例对象的再封装,使其具有弹窗提示和错误处理的功能等操作。 1. 导入相关模块 ```TypeScript // MediaPlayer.ets import emitter from '@ohos.events.emitter'; import media from '@ohos.multimedia.media'; import { BusinessError } from '@ohos.base'; import promptAction from '@ohos.promptAction'; import Logger from '../utils/log/Logger'; import Constants from '../utils/constants/Constants'; import { BOTTOM } from '../utils/constants/UIStyle'; ``` 2. 定义一个默认导出的类 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... } ``` 3. 类中的属性和方法 | 属性 | 方法 | | -------------------------------------------- | ---------------------------------------------------- | | **private** avPlayer: media.AVPlayer \| null | **constructor**() | | **private** surfaceId: string | **private** showToast(message: string) | | **private** url: string | **private** promptActionDisplay(expectState: string) | | isUser: boolean | **async** initAvPlayer() | | curState: number | prepareForVideo(Path: string, xSurface: string) | | | setAVPlayerCallback() | | | delAVPlayerCallback() | | | **async** initialize(self: MediaPlayer) | | | **async** prepare(self: MediaPlayer) | | | **async** play(self: MediaPlayer) | | | **async** pause(self: MediaPlayer) | | | **async** stop(self: MediaPlayer) | | | **async** reset(self: MediaPlayer) | | | **async** release(self: MediaPlayer) | | | **async** seek(seekTime: number, mode: number) | | | getCurDuration() | | | getDuration() | | | getState() | a. 属性及构造函数 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { private avPlayer: media.AVPlayer | null; // avPlayer对象,用来管理和播放媒体资源,包括音频和视频 private surfaceId: string; // 视频播放窗口的id,用于视频播放的窗口渲染 private url: string; // 媒体url地址 isUser: boolean; // 判断当前是否为用户模式,默认为true;不是用户模式即为开发者模式 curState: number; // 当前状态 constructor() { this.avPlayer = null; this.surfaceId = Constants.DEFAULT_STRING; this.url = Constants.DEFAULT_STRING; this.isUser = true; this.curState = Constants.IS_IDLE; }; ... } ``` b. 对于弹窗的封装 对于`showToast`,其实就是调用常规的promptAction.showToast(),将其封装成只关注弹窗信息的传入,而持续时间和在屏幕上的位置则是规定好的,并添加上了错误的捕获和处理。 对于`promptActionDisplay`,此函数接受一个期望中的状态值,也就是在用户执行某些操作后期望AVPlayer对象进入到的状态值。当当前状态与期望状态一致时,弹窗显示当前状态值;不一致时,提示未进入到期待状态,并展示当前状态值。 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... /** * 对提示弹窗的封装 * @param message 弹窗呈现给用户的提示信息 */ private showToast(message: string) { // try {...} catch(err) {...} 块捕获可能出现的错误 try { promptAction.showToast({ message: message, // 弹窗中的具体信息 duration: Constants.DURATION, // 弹窗持续时间,以ms为单位 bottom: BOTTOM // 弹窗与屏幕底部的距离 }); } catch (error) { let message = (error as BusinessError).message let code = (error as BusinessError).code // 在控制台输出捕获到的错误信息 console.error(`showToast args error code is ${code}, message is ${message}`); }; } /** * 提示当前的状态信息,告知用户是否已进入期待的状态 * @param expectState 期待达到的状态 */ private promptActionDisplay(expectState: string) { if (this.avPlayer) { if (this.avPlayer.state == expectState) { this.showToast(expectState); } else { this.showToast(`未进入${expectState},当前状态为` + this.avPlayer.state); } } else { // 在控制台打印日志,和console.info作用类似 Logger.info(Constants.AVPLAYER_IS_NULL); } } } ``` c. 包装createAVPlayer(),提供相应的错误处理 | **函数接口** | **具体说明** | | ----------------------------------- | ------------------------------------------------------------ | | createAVPlayer(): Promise | 异步方式创建音视频播放实例,通过Promise获取返回值。Promise:Promise对象。异步返回AVPlayer实例,失败时返回null。**注意:**一般来说,可创建的视频播放实例不能超过13个,而可创建的音视频播放实例(即音频、视频、音视频三类相加)不能超过16个。但具体实例数量依赖于设备芯片支持情况,可能会少于上述数量。 | 执行完createAVPlayer()后进入到.then()中作进一步处理,如果成功创建,则返回AVPlayer对象实例,失败则返回null(创建失败并不意味着发生错误,所以Promise为兑现状态),都由.then()中第一个回调函数的参数进行接收。如果不是null,则存储于类属性avPlayer中,并且订阅相关的事件,弹窗提示创建AVPlayer实例成功。而如果在createAVPlayer()或.then()中遇到任何错误,会被.catch()捕获,并依据所写函数进行错误处理,一般是打印错误信息。 > 简单来说,可以认为是.then(onFulfilled?: (value) => {}, onRejected?: (reason) => {}),此函数接收两个可选的参数,而且参数类型都是一个回调函数。第一个参数是 Promise 兑现时的回调函数,第二个参数是 Promise 拒绝时(常是发生错误时)的回调函数,一般第二个回调函数可以认为是.catch()的替换,两者选其一来写就行。 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... /** * 初始化播放器 */ async initAvPlayer() { // 创建avPlayer实例对象 await media.createAVPlayer() .then((video) => { if (video != null) { this.avPlayer = video; this.setAVPlayerCallback(); Logger.info(Constants.CREATE_AVPLAYER_SUCCESS); this.showToast(Constants.CREATE_AVPLAYER_SUCCESS); } else { Logger.info(Constants.CREATE_AVPLAYER_FAIL); } }) .catch((error: BusinessError) => { Logger.error(`AVPlayer catchCallback, error:${error}`); this.avPlayer = null; }); }; ... } ``` d. 提前存储好播放视频需要设置的两个属性值,以便在需要的时候能够直接使用 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... /** * 准备视频播放器必需的两个配置 * @param Path 文件地址 fd://number 或 网络播放低地址 http://... or https://... * @param xSurface XComponent - SurfaceId */ prepareForVideo(Path: string, xSurface: string) { this.url = Path; this.surfaceId = xSurface; }; ... } ``` e. 包装对各种事件订阅和取消订阅 一共包含五个事件,'error'、'stateChange'(**核心**)、'timeUpdate'、'endOfStream'、'seekDone',这些都是AVPlayer类里面已有定义的事件订阅函数,以下是对这些函数的具体说明。 | **函数接口** | **具体说明** | | ------------------------------------------------------------ | ------------------------------------------------------------ | | 通式:on(type: string, callback: ...): void [...代表不确定类型,依据type值不同,回调函数类型不同] | 类似于一个正常的函数,函数名为on。第一个参数是指定需要订阅的事件,为字符串类型;第二个参数是为此事件注册的回调函数,在使用官方封好的订阅函数去订阅事件时,回调函数的类型根据事件的不同是确定好的。没有返回值。 | | on(type: 'error', callback: ErrorCallback): void | 监听AVPlayer错误事件,该事件仅用于错误提示,不需要用户停止播控动作。监听到错误不等同于AVPlayer进入error状态,只有进入error状态才需要用户主动去使用reset或release方法退出播放操作。 | | on(type: 'stateChange', callback: (state: AVPlayerState, reason: StateChangeReason) => void): void | 监听播放状态机AVPlayerState切换的事件。 | | on(type: 'timeUpdate', callback: Callback): void | 监听资源播放当前时间,单位为毫秒(ms),用于刷新进度条当前位置,默认间隔100ms时间上报。如果因用户操作(即使用了seek方法)产生的时间变化会立刻上报。 | | on(type: 'endOfStream', callback: Callback): void | 监听资源播放至结尾的事件。 | | on(type: 'seekDone', callback: Callback): void | 监听seek方法生效的事件。 | | **函数接口** | **具体说明** | | ------------------------------ | ------------------------------------------------------------ | | 通式:off(type: string): void | 与on相对,一般用于在销毁对象或不再使用此功能后取消相关的事件订阅,也就是当该事件触发时不会再对该事件响应相应的回调函数。 | | off(type: 'error'): void | 取消监听播放的错误事件。 | | off(type: 'stateChange'): void | 取消监听播放状态机AVPlayerState切换的事件。 | | off(type: 'timeUpdate'): void | 取消监听资源播放当前时间。 | | off(type: 'endOfStream'): void | 取消监听资源播放至结尾的事件。 | | off(type: 'seekDone'): void | 取消监听seek生效的事件。 | #### Emitter介绍 存放在@ohos.events.emitter模块中,使用前需要先导入模块: ```TypeScript import emitter from '@ohos.events.emitter'; ``` 上面的事件订阅都是api内置的,而emitter相当于给到开发者自己定义事件的能力,在本次案例的复现中,主要用到的是事件的推送和事件的订阅,分别对应于emitter.on和emitter.emit。 + **emitter.on** | **函数接口** | **具体说明** | | ---------------------------------------------------------- | ------------------------------------------------------------ | | on(event: InnerEvent, callback: Callback): void | event:持续订阅的事件,其中EventPriority,在订阅事件时无需指定,也不生效。callback:接收到该事件时需要执行的回调处理函数。**持续订阅指定的事件,并在接收到该事件时,执行对应的****回调****处理函数。** | **InnerEvent类型**:订阅或发送的事件,订阅事件时`EventPriority`不生效。 ```TypeScript interface InnerEvent { eventId: number, // 事件ID,由开发者定义用来辨别事件。 priority?: EventPriority // 事件被投递的优先级。 } ``` 这是一种对象类型(像一个没有方法的类),赋值时`let innerEvent: emitter.InnerEvent = { eventId: 1 };`。其中eventId是必填,而priority可选。 **EventData类型:**发送事件时传递的数据。 在本案例中不涉及事件数据的传递,简单来说就是推送时携带数据,然后在触发回调函数时将数据给到回调函数进行处理,具体细节不在此介绍。 + **emitter.emit** | **函数接口** | **具体说明** | | ----------------------------------------------- | ------------------------------------------------------------ | | emit(event: InnerEvent, data?: EventData): void | event:发送的事件,其中EventPriority用于指定事件被发送的优先级。callback:事件携带的数据。**发送指定的事件到事件队列当中。** | **EventPriority类型**:用于表示事件被发送的优先级。 ```TypeScript enum EventPriority { // 枚举类型的值往上找最近定义的具体数值往后递增,直到遇到下一个具体定义的数值 IMMEDIATE = 0, // 表示事件被立即投递。(本次案例中,都使用立即发送) HIGH, // 值为1,表示事件先于LOW优先级投递。 LOW, // 值为2,表示事件优于IDLE优先级投递,事件的默认优先级是LOW。 IDLE // 值为3,表示在没有其他事件的情况下,才投递该事件。 } ``` 这是一种枚举类型,可读不可写,使用时通过例如`EventPriority.IMMEDIATE`来获取,赋值时`let eventPriority: EventPriority = EventPriority.IMMEDIATE;`。 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... /** * 注册avplayer回调函数 */ setAVPlayerCallback() { if (this.avPlayer) { // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程 this.avPlayer.on('error', (err) => { Logger.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); if (this.avPlayer) this.avPlayer.reset(); // 调用reset重置资源,触发idle状态 }) // 状态机变化回调函数 this.avPlayer.on('stateChange', async (state, reason) => { Logger.info("the reason of stateChange is" + reason); if (this.avPlayer) { switch (state) { case Constants.AVPLAYER_STATE_IDLE: // 成功调用reset接口后触发该状态机上报 Logger.info(Constants.IDLE_CALLED); this.curState = Constants.IS_IDLE; break; case Constants.AVPLAYER_STATE_INITIALIZED: // avplayer 设置播放源后触发该状态上报 Logger.info(Constants.INITIALIZED_CALLED); this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置 if (this.isUser) { this.prepare(this); } break; case Constants.AVPLAYER_STATE_PREPARED: // prepare调用成功后上报该状态机 Logger.info(Constants.PREPARED_CALLED); if (this.isUser) { this.play(this); } break; case Constants.AVPLAYER_STATE_PLAYING: // play成功调用后触发该状态机上报 Logger.info(Constants.PLAYING_CALLED); this.curState = Constants.IS_PLAYING; emitter.emit(Constants.PLAYER_PLAY_SUCCESS_EVENT); break; case Constants.AVPLAYER_STATE_PAUSED: // pause成功调用后触发该状态机上报 Logger.info(Constants.PAUSED_CALLED); this.curState = Constants.IS_PAUSED; emitter.emit(Constants.PLAYER_PAUSE_SUCCESS_EVENT); break; case Constants.AVPLAYER_STATE_COMPLETED: // 播放结束后触发该状态机上报 Logger.info(Constants.COMPLETED_CALLED); emitter.emit(Constants.PLAYER_COMPLETE_SUCCESS_EVENT); if (this.isUser) { this.stop(this); } break; case Constants.AVPLAYER_STATE_STOPPED: // stop成功调用后触发该状态机上报 Logger.info(Constants.STOPPED_CALLED); emitter.emit(Constants.PLAYER_PAUSE_SUCCESS_EVENT) ; if (this.isUser) { this.prepare(this); } break; case Constants.AVPLAYER_STATE_RELEASED: // release成功调用后触发该状态机上报 Logger.info(Constants.RELEASED_CALLED); break; case Constants.AVPLAYER_STATE_ERROR: Logger.info(Constants.ERROR_CALLED); break; default: Logger.info('Avplayer unknown state :' + state); break; } } }) // 时间上报监听函数 this.avPlayer.on('timeUpdate', (time: number) => { emitter.emit(Constants.UPDATE_PROGRESS_BAR_EVENT); // 通知进度条更新 Logger.info('Avplayer timeUpdate success,and new time is :' + time); }) // 视频播放结束触发回调 this.avPlayer.on('endOfStream', () => { Logger.info('Avplayer endOfStream success'); }) // seek操作回调函数 this.avPlayer.on('seekDone', (seekDoneTime: number) => { Logger.info('Avplayer seekDone success,and seek time is:' + seekDoneTime); }) Logger.info(Constants.SET_HINT); } else { Logger.info(Constants.AVPLAYER_IS_NULL); } }; /** * 取消订阅avplayer相关事件 */ delAVPlayerCallback() { if (this.avPlayer) { this.avPlayer.off("error"); this.avPlayer.off("stateChange"); this.avPlayer.off("timeUpdate"); this.avPlayer.off("endOfStream"); this.avPlayer.off("seekDone"); Logger.info(Constants.DEL_HINT); } else { Logger.info(Constants.AVPLAYER_IS_NULL); } }; ... } ``` 接下来主要对setAVPlayerCallback()中的各个事件注册的回调函数进行分析。 + on('stateChange') 通过switch/case语句来实现各个状态下所应该完成的操作,在响应事件时会获取到携带的数据,表示当前的状态,我们自定义一个变量来承接。 如果当前状态是"idle",设置curState(播放器状态)为“空”。 如果当前状态是"initialized",设置AVPlayer对象的属性surfaceId,绑定视频播放窗口。如果当前是用户模式,则立刻调用prepare()。 如果当前状态是"prepared",并且如果当前是用户模式,则立刻调用paly()。 如果当前状态是"playing",设置curState(播放器状态)为“已暂停”,并推送“播放成功事件”。 如果当前状态是"paused",设置curState(播放器状态)为“正在播放”,并推送“暂停成功事件”。 如果当前状态是"completed",推送“播放完成事件”。如果当前是用户模式,则立刻调用stop()。 如果当前状态是"stopped",推送“暂停成功事件”。如果当前是用户模式,则立刻调用prepare()。 如果当前状态是"released"、"error",或以上都不是,打印状态信息日志。 + on('timeUpdate') 在响应事件时会获取到携带的数据,表示当前的视频时长。 推送“进度条更新事件”。 + on('endOfStream')、on('seekDone') 打印相关信息日志。 f. 包装AVPlayer对象关于切换状态、跳转时长的常用方法 具体介绍一下AVPlayer对象关于切换状态的各个方法,也是最核心的部分。 | **函数接口** | **具体说明** | | ------------------------ | ------------------------------------------------------------ | | prepare(): Promise | 准备播放音频/视频,当前为initialized状态调用。 | | play(): Promise | 开始播放音视频资源,只能在prepared/paused/completed状态调用。 | | pause(): Promise | 暂停播放音视频资源,只能在playing状态调用。 | | stop(): Promise | 停止播放音视频资源,只能在prepared/playing/paused/completed状态调用。 | | reset(): Promise | 重置播放,只能在initialized/prepared/playing/paused/completed/stopped/error状态调用。 | | release(): Promise | 销毁播放资源,除released状态,都可以调用。 | 开发者无需关心底层的实现逻辑,只需要知道,如果在这些函数的调用中没有出现错误,就代表着成功执行了对应的功能。这些函数都是通过Promise获取返回值,void代表着Promise返回时不携带任何类型的数据结果,即在.then()的第一个回调函数中无需设置参数进行接收。虽然API10也提供这些函数的另外一种接口,以异步回调函数作为参数,无返回值,但是建议在开发中使用Promise形式的接口,因为Promise的出现就是为了解决使用普通回调函数可能出现的回调地狱问题。 使用async/await可以用同步代码编写的逻辑去编写异步代码,在这里主要是为了等待异步操作的完成,调用promptActionDisplay()必须明确在异步函数完成之后。异步函数没有完成,就代表着当前状态还没有定论,在这种未定态下去判断当前状态明显是不可取的。 ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... /** * 初始化,配置avPlayer的url */ async initialize(self: MediaPlayer) { if (self.avPlayer) { self.avPlayer.url = self.url; Logger.info("Avplayer url: " + self.avPlayer.url); self.promptActionDisplay(Constants.AVPLAYER_STATE_INITIALIZED); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } } /** * 准备,加载流资源 */ async prepare(self: MediaPlayer) { if (self.avPlayer) { await self.avPlayer.prepare().then(() => { Logger.info(Constants.PREPARE_SUCCESS); }, (err: BusinessError) => { Logger.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); }); self.promptActionDisplay(Constants.AVPLAYER_STATE_PREPARED); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; /** * 开始播放 */ async play(self: MediaPlayer) { if (self.avPlayer) { await self.avPlayer.play().then(() => { Logger.info(Constants.PLAY_SUCCESS); }, (err: BusinessError) => { Logger.error(`Invoke play failed, code is ${err.code}, message is ${err.message}`); }); self.promptActionDisplay(Constants.AVPLAYER_STATE_PLAYING); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; /** * 暂停 */ async pause(self: MediaPlayer) { if (self.avPlayer) { await self.avPlayer.pause().then(() => { Logger.info(Constants.PAUSE_SUCCESS); }, (err: BusinessError) => { Logger.error(`Invoke pause failed, code is ${err.code}, message is ${err.message}`); }); self.promptActionDisplay(Constants.AVPLAYER_STATE_PAUSED); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; /** * 终止 */ async stop(self: MediaPlayer) { if (self.avPlayer) { await self.avPlayer.stop().then(() => { Logger.info(Constants.STOP_SUCCESS); }, (err: BusinessError) => { Logger.error(`Invoke stop failed, code is ${err.code}, message is ${err.message}`); }); self.promptActionDisplay(Constants.AVPLAYER_STATE_STOPPED); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; /** * 重置 */ async reset(self: MediaPlayer) { if (self.avPlayer) { await self.avPlayer.reset().then(() => { Logger.info(Constants.RESET_SUCCESS); }, (err: BusinessError) => { Logger.error(`Invoke reset failed, code is ${err.code}, message is ${err.message}`); }); self.promptActionDisplay(Constants.AVPLAYER_STATE_IDLE); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; /** * 销毁 */ async release(self: MediaPlayer) { if (self.avPlayer) { await self.avPlayer.release().then(() => { Logger.info(Constants.RELEASE_SUCCESS); self.delAVPlayerCallback(); }, (err: BusinessError) => { Logger.error(`Invoke release failed, code is ${err.code}, message is ${err.message}`); }); self.promptActionDisplay(Constants.AVPLAYER_STATE_RELEASED); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; /** * 快进 / 后退 * @param seekTime 快进(后退到指定时间(ms) * @param mode 快进 or 后退 */ async seek(seekTime: number, mode: number) { if (this.avPlayer) { this.avPlayer.seek(seekTime, mode); } else { Logger.error(Constants.AVPLAYER_IS_NULL); } }; ... } ``` g. 对外提供获取AVPlayer属性的方法(虽然可以直接获取,但是不够安全) ```TypeScript // MediaPlayer.ets export default class MediaPlayer { ... getCurDuration() { if (this.avPlayer) { return this.avPlayer.currentTime; } else { Logger.error(Constants.AVPLAYER_IS_NULL); } return Constants.INIT_NUM; }; getDuration() { if (this.avPlayer) { return this.avPlayer.duration; } else { Logger.error(Constants.AVPLAYER_IS_NULL); } return Constants.INIT_NUM; }; getState() { if (this.avPlayer) { return this.avPlayer.state; } else { Logger.error(Constants.AVPLAYER_IS_NULL); } return Constants.NONE_STATE; }; } ``` ### 8.3.5 模块功能页面 ![1722325704128-1](./README.assets/1722325704128-1.jpeg) ![1722325704128-2](./README.assets/1722325704128-2.jpeg) 新建GetFromInternet.ets页面,表示该页面用来演示从网络获取视频播放的功能。这个页面主要由四部分构成,分别是返回上一页、下拉输入框设置url、视频播放窗口以及视频操作控制台。 为更清晰地使读者了解视频播放的全流程控制,我们忽略掉具体UI细节的介绍,仅作罗列和简单说明,更多专注于梳理内部调用的逻辑,所以在控制台介绍中仅对“开发者模式”作介绍,“用户模式”就是将开发者模式中的各个操作串联起来,符合日常使用的逻辑。 以下是此页面中关乎于UI组件的关系结构图: ![whiteboard_exported_image (9)](./README.assets/whiteboard_exported_image%20(9).png) #### 8.3.5.1 主体组件 1. 导入相关模块 ```TypeScript // GetFromInternet.ets import { BusinessError } from '@ohos.base'; import promptAction from '@ohos.promptAction'; import emitter from '@ohos.events.emitter'; import MediaPlayer from '../model/MediaPlayer'; import Constants from '../utils/constants/Constants'; import TimeUtil from '../utils/tool/TimeUtils'; import Logger from '../utils/log/Logger'; import { BOTTOM, Color, Height, HEIGHT, Padding, Width, WIDTH } from '../utils/constants/UIStyle'; import DropDownInputBox from '../view/DropDownInputBox'; import ActSelect from '../utils/struct/ActSelect'; import Return from '../view/Return'; import PlayWindow from '../view/PlayWindow'; import SelectMode from '../view/SelectMode'; import Operator from '../view/Operator'; ``` 2. 组件内容 | **属性** | **方法** | | ------------------------------------------------------ | ------------------------------------------------------------ | | @State text: string | **private** updateProgressBarCallback: () => void | | @State isPullDown: boolean | **private** playerPlaySuccessCallback: () => void | | @State isSelect_1: ActSelect | **private** playerPauseSuccessCallback: () => void | | @State isSelect_2: ActSelect | **private** playerCompleteSuccessCallback: () => void | | @State isUser: boolean | initListener() | | @State curDuration: number | relListener() | | @State totalDuration: number | aboutToAppear() | | @State curTime: string | aboutToDisappear() | | @State totalTime: string | onPageShow() | | @State curValue: number | onPageHide() | | @State preValue: number | startPlay() | | @State playIcon: Resource | **async** onClickFunc(desState: string, operaName: string, func: (self: MediaPlayer) => Promise) | | @State playQuickIcon: Resource | showToast(message: string) | | @State isSeekingGesture: boolean | playerControllerTransientDisplay() | | @State playerControllerVisibility: Visibility | @Builder Panel() | | @State playQuickIconVisibility: Visibility | @Builder Subject() | | **private** videoPlayer: MediaPlayer \| null | build() | | **private** xComponentController: XComponentController | initialize: () => void | | **private** panOption: PanGestureOptions | prepare: () => void | | **private** offsetX: number | play: () => void | | | pause: () => void | | | stop: () => void | | | reset: () => void | | | release: () => void | 3. 属性介绍 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { @State text: string = Constants.DEFAULT_STRING // 输入的视频播放地址 @State isPullDown: boolean = false // 判断下拉框是否处于下拉状态 @State isSelect_1: ActSelect = new ActSelect(false) // 预置的第一个内容是否被选中,封装进一个对象类型,以其作为代理实现引用传值 @State isSelect_2: ActSelect = new ActSelect(false) // 预置的第二个内容是否被选中,封装进一个对象类型,以其作为代理实现引用传值 @State isUser: boolean = true // 当前是否选择用户模式,否则为开发者模式 @State curDuration: number = Constants.INIT_NUM; // 视频当前已播放时长,以ms计算 @State totalDuration: number = Constants.INIT_NUM; // 视频总时长,以ms计算 @State curTime: string = Constants.DEFAULT_TIME; // 视频当前播放时长,格式化为”分钟:秒数“的字符串形式 @State totalTime: string = Constants.DEFAULT_TIME; // 视频总时长,格式化为”分钟:秒数“的字符串形式 @State curValue: number = Constants.INIT_NUM; // 当前时长占总时长的百分比,因为Progress组件默认的总长为100 @State preValue: number = Constants.INIT_NUM; // 手指移动前时长占总时长的百分比,因为Progress组件默认的总长为100 @State playIcon: Resource = Constants.PLAYER_PLAY_ICON; // 播放或暂停图标 @State playQuickIcon: Resource = Constants.PLAYER_FORWARD_QUICK_ICON; // 快进或快退图标 @State isSeekingGesture: boolean = false; // 是否为触发跳转的手势(即平移手势) @State playerControllerVisibility: Visibility = Visibility.Visible; // 进度条显隐控制 @State playQuickIconVisibility: Visibility = Visibility.Hidden; // 快进快退图标显隐控制 private videoPlayer: MediaPlayer | null = null; // 自定义类的对象实例,包含最重要的AVPlayer对象 private xComponentController: XComponentController = new XComponentController(); // XComponent组件的控制器 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right }); // 设置滑动手势识别器的属性 private offsetX: number = Constants.INIT_NUM; // 手势事件x轴相对偏移量 ... } ``` 4. 方法介绍 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... // 以下四个为自定义的四个事件要注册的回调函数 /** * 进度条更新回调 */ private updateProgressBarCallback: () => void /** * 成功播放回调 */ private playerPlaySuccessCallback: () => void /** * 暂停播放回调 */ private playerPauseSuccessCallback: () => void /** * 播放结束回调 */ private playerCompleteSuccessCallback: () => void /** * 初始化自定义事件监听,根据播放器状态反馈来更新UI,也就是为四个事件注册回调函数 */ initListener() /** * 取消针对自定义事件ID的订阅 */ relListener() // 以下四个为生命周期函数,在程序中无需自己调用 /** * 在将要创建此组件时调用,这里用于注册事件回调函数,initListener */ aboutToAppear() /** * 在将要销毁此组件时调用,这里用于取消对此事件的订阅,relListener */ aboutToDisappear() /** * 在页面显示于前台时调用,这里用于初始化this.videoPlayer,也就是创建一个MediaPlayer类的实例。 * 调用类实例的initAvPlayer()函数,创建一个AVPlayer对象,与播放引擎关联。 * 在三秒后使得视频窗口中的进度控制板由显示转为隐藏。 */ onPageShow() /** * 调用类实例的release()函数,释放关联的播放引擎资源。 */ onPageHide() /** * 一键播放按钮点击时调用的函数 */ startPlay() /** * 开发者模式下各个按钮绑定的回调函数 * @param desState 目标到达的状态 * @param operaName 当前的操作名称 * @param func 满足条件最本质要调用的方法 * @param self 调用的方法处于的类对象,将自身传入,以解决this指向不明确的问题 * @param curStates 能够成功调用目标方法的几种当前状态 */ async onClickFunc(desState: string, operaName: string, func: (self: MediaPlayer) => Promise, self: MediaPlayer, ...curStates: Array) /** * 对promptAction.showToast()的封装。默认持续1.5s,距离底部20vp * @param message 弹窗提示的文本信息 */ showToast(message: string) /** * 播放控制组件(播放/暂停按键、进度条等)的短暂显示 */ playerControllerTransientDisplay() // 自定义组件函数,轻量级别的@Component @Builder Panel() @Builder Subject() build() // 以下七个函数是对点击开发者模式下各个按钮的回调函数的简单封装,主要是为了统一为变量方便传递给子组件 /** * 准备url和surfaceId * 对this.onClickFunc的调用 */ initialize: () => void /** * 因为执行AVPlayer对象的prepare()函数时间较长,弹窗提示用户耐心等待 * 对this.onClickFunc的调用 */ prepare: () => void /** * 对this.onClickFunc的调用 */ play: () => void /** * 对this.onClickFunc的调用 */ pause: () => void /** * 对this.onClickFunc的调用 */ stop: () => void /** * 对this.onClickFunc的调用 */ reset: () => void /** * 对this.onClickFunc的调用 */ release: () => void } ``` 5. 初始化工作 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... /** * 初始化自定义事件监听,根据播放器状态反馈来更新UI */ initListener() { emitter.on(Constants.UPDATE_PROGRESS_BAR_EVENT, this.updateProgressBarCallback); emitter.on(Constants.PLAYER_PLAY_SUCCESS_EVENT, this.playerPlaySuccessCallback); emitter.on(Constants.PLAYER_PAUSE_SUCCESS_EVENT, this.playerPauseSuccessCallback); emitter.on(Constants.PLAYER_COMPLETE_SUCCESS_EVENT, this.playerCompleteSuccessCallback); } /** * 取消针对自定义事件ID的订阅 */ relListener() { emitter.off(Constants.UPDATE_PROGRESS_BAR); emitter.off(Constants.PLAYER_PLAY_SUCCESS); emitter.off(Constants.PLAYER_PAUSE_SUCCESS); emitter.off(Constants.PLAYER_COMPLETE_SUCCESS); } aboutToAppear() { this.initListener(); } aboutToDisappear() { this.relListener(); } onPageShow() { this.videoPlayer = new MediaPlayer(); this.videoPlayer.initAvPlayer(); setTimeout(() => { this.playerControllerVisibility = Visibility.Hidden; }, Constants.TRANSIENT_DISPLAY_TIME); } onPageHide() { if (this.videoPlayer) this.videoPlayer.release(this.videoPlayer); } ... } ``` 代码解读:在GetFromInternet组件创建的时候,为自定义的四个事件注册回调函数;在GetFromInternet组件销毁的时候,取消对自定义的四个事件的订阅。在页面呈现在前台时,由GetFromInternet的videoPlayer属性作为视频播放业务类MediaPlayer的对象,接下来的所有关于视频播放的操作,都由videoPlayer来调用和控制;在页面关闭或置于后台时,调用videoPlayer的release方法,释放与videoPlayer中的avPlayer属性关联的播放引擎资源。 #### 8.3.5.2 重点功能介绍 ##### **获取url地址** ![1722325704128-3](./README.assets/1722325704128-3.png) 下拉输入框的核心代码放在`DropDownInputBox.ets`文件下,实现的功能是可以自己输入网络地址或是选择预置的两个可用的测试地址设置想要播放的视频资源。当输入框内容发生改变的时候,会将输入框的内容同步到GetFromInternet组件的text属性。点击获取按钮,如果当前AVPlayer对象的状态不是"idle",则调用reset()方法恢复至"idle"状态,然后校验当前输入框中的链接地址是否为一个网络地址,如果是,则弹窗提示“url获取成功”,代表当前存放在text属性中的值是一个合理的网络地址。 点击“获取”按钮绑定的回调函数如下: ```TypeScript // DropDownInputBox.ets @Component export default struct DropDownInputBox { ... private getUrl: () => void = () => { if (this.videoPlayer && this.videoPlayer.getState() != Constants.AVPLAYER_STATE_IDLE) { this.videoPlayer.reset(this.videoPlayer); } if (!(this.text.includes(Constants.NETWORK_LINK_TAG_1) || this.text.includes(Constants.NETWORK_LINK_TAG_2)) && this.text) { this.showToast(Constants.URL_IS_WRONG); } else if (this.text) { Logger.info(Constants.GET_URL_SUCCESS); this.showToast(Constants.GET_URL_SUCCESS); } else { this.showToast(Constants.GET_URL_FAIL); } } ... } ``` 效果图: ![1722325704128-4](./README.assets/1722325704128-4.png) ##### **视频窗口** ![1722325704128-5](./README.assets/1722325704128-5.png) 视频窗口包含两大部分,分别是窗口和进度控制板。 **窗口**由XComponent组件创建,以下是XComponent的内容: ```TypeScript // PlayWindow.ets @Extend(XComponent) function window(onLoadFunc: () => void) { .onLoad(onLoadFunc) .width(Width.WINDOW) .height(Height.WINDOW) .borderRadius(Radius.WINDOW) } @Component export default struct PlayWindow { ... build() { Stack() { XComponent({ id: Constants.X_COMPONENT_ID, type: Constants.X_COMPONENT_TYPE, controller: this.xComponentController }) .window(() => { this.xComponentController.setXComponentSurfaceSize({ surfaceWidth: Width.SURFACE, surfaceHeight: Height.SURFACE }); }) ... } } ... } ``` 其中,使用XComponent需要设置三个属性,id无关紧要,一般来说可以随意设置;type我们这里选择"surface",因为需求是通过XComponent来渲染视频内容;controller则是设置XComponent组件的控制器,这里设置的是从GetFromInternet传递过来的xComponentController属性。 XComponent与播放视频有关的操作十分简单,就只是提供一个surfaceId,每个surface类型的XComponent都有属于自己的surfaceId,在需要的时候通过控制器的getXComponentSurfaceId()方法即可获取。将获取到的XComponent的surfaceId赋值给AVPlayer对象的surfaceId属性,如此一来就绑定好了播放引擎的视频窗口,视频内容将会在XComponent组件中逐帧渲染呈现。 **进度控制板**由播放/暂停键、快进/快退能力以及进度条时长显示三部分构成。 1. 播放/暂停键 ```TypeScript // PlayControl.ets ... onClickFunc: () => void = () => { if (this.videoPlayer) { if (this.videoPlayer.curState == Constants.IS_PLAYING) { this.videoPlayer.pause(this.videoPlayer); } else if (this.videoPlayer.curState == Constants.IS_PAUSED) { this.videoPlayer.play(this.videoPlayer); } else if (this.videoPlayer.curState == Constants.IS_IDLE) { this.startPlay(); } } } ... ``` 以上是播放/暂停键图标所绑定的点击回调函数,如果当前状态为"playing",则点击暂停;如果当前状态为"paused",则点击播放;如果当前状态为"idle",则点击一键播放。 2. 快进/快退能力 此能力的实现与在整个视频窗口组件即PlayWindow所绑定的手势操作有关。先来介绍一下**手势操作**: 手势操作依托于组件,基础用法是通过gesture属性给组件绑定不同类型的手势事件,并设置事件的相应方法。 | **属性** | **具体说明** | | ------------------------------------------------- | ------------------------------------------------------------ | | gesture(gesture: GestureType, mask?: GestureMask) | gesture: 绑定的手势类型。mask: 事件响应设置。**提供最基本的绑定手势能力,通过gesture来设置,不同的手势类型有不同的事件。mask在本案例中不涉及,不作具体介绍,默认值为GestureMask.Normal。** | GestureType类型:包含点击、长按、平移、捏合、旋转、滑动手势类型,这里介绍TapGesture(点击手势)和PanGesture(平移手势),分别对应点击唤出进度控制板和平移调整视频进度的能力。 + TapGesture:`TapGesture(value?: { count?: number, fingers?: number })` 接收一个对象类型的值作为参数。count用来设定识别的连续点击次数;fingers用来设定触发点击的手指数,两者共同决定满足多少根手指同时点击几次会被判断为点击手势触发,从而在触发后执行相应的回调函数。 具有一个事件`onAction(event: (event: ``GestureEvent``) => void)`,即点击手势触发后则会响应的回调函数。接收一个以GestureEvent类型的值为参数的回调函数作为参数,GestureEvent类型的值包含各种触发手势时携带的信息。GestureEvent对象说明可前往官方文档中查阅,此处不一一说明。点击手势触发事件一般可以不使用到这些数据信息,也可以传递一个不包含参数的箭头函数作为回调函数。 > [各种手势事件](https://docs.openharmony.cn/pages/v4.1/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-gesture-settings.md#gestureevent%E5%AF%B9%E8%B1%A1%E8%AF%B4%E6%98%8E) + PanGesture:`PanGesture(value?: { fingers?: number, direction?: PanDirection, distance?: number })` 接收一个对象类型的值作为参数。fingers用于指定触发拖动的最少手指数;direction指定触发拖动的手势方向;distance指定触发拖动手势事件的最小拖动距离,共同决定满足多少根手指同时往某个方向拖动多少距离会被判断为平移手势触发,从而在触发后执行相应的回调函数。 > PanDirection类型:枚举类型,本案例使用:PanDirection.Left | PanDirection.Right,向左拖动或向右拖动。 介绍三个事件onActionStart、onActionUpdate、onActionEnd。 + `onActionStart(event: (event: ``GestureEvent``) => void)`平移手势识别成功响应的回调。 + `onActionUpdate(event: (event: ``GestureEvent``) => void)`平移手势平移过程中持续响应的回调。 + `onActionEnd(event: (event: ``GestureEvent``) => void)`平移手势平移结束手指抬起时响应的回调。 左移或右移的平移手势一般会使用到GestureEvent对象中的offsetX属性,这里用来计算拖动距离,反馈到进度条的刷新。 **AVPlayer的seek()方法** | **函数接口** | **具体说明** | | ------------------------------------------ | ------------------------------------------------------------ | | seek(timeMs: number, mode?:SeekMode): void | timeMs: number,指定的跳转时间节点,单位毫秒(ms),取值范围为[0, duration]。duration就是AVPlayer的duration属性。mode: SeekMode,基于视频I帧的跳转模式。SeekMode是枚举类型,SeekMode.SEEK_NEXT_SYNC适合快退,SeekMode.SEEK_PREV_SYNC适合快进。**跳转到指定播放位置,一般在playing状态调用。** | ```TypeScript // PlayWindow.ets ... .gesture( PanGesture(panOption) .onActionStart(onActionStartFunc) .onActionUpdate(onActionUpdateFunc) .onActionEnd(onActionEndFunc) ) .gesture( TapGesture({ count: Constants.CLICK_RESPOND }) .onAction(onActionFunc) ) ... ``` 以上是PlayWindow整个视频窗口组件绑定的手势操作,绑定了两个手势操作,分别是点击手势和平移手势,以下是对手势绑定的回调函数的具体说明。 ```TypeScript // PlayWindow.ets @Component export default struct PlayWindow { ... private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right }); onActionStartFunc: (event: GestureEvent) => void = (event) => { this.isSeekingGesture = true; this.playerControllerVisibility = Visibility.Visible; this.playQuickIconVisibility = Visibility.Visible; this.offsetX = event.offsetX; Logger.info(Constants.SEEKING_START); } onActionUpdateFunc: (event: GestureEvent) => void = (event) => { let mode = media.SeekMode.SEEK_PREV_SYNC; this.playQuickIcon = Constants.PLAYER_FORWARD_QUICK_ICON; if (event.offsetX - this.offsetX <= Constants.INIT_NUM) { mode = media.SeekMode.SEEK_NEXT_SYNC; this.playQuickIcon = Constants.PLAYER_BACKWARD_QUICK_ICON; } this.curValue = this.preValue + (event.offsetX - this.offsetX) * Constants.PERCENTAGE_DIV; if (this.videoPlayer) this.videoPlayer.seek(this.totalDuration * (this.curValue / Constants.PERCENTAGE_MULTI), mode); } onActionEndFunc: () => void = () => { this.isSeekingGesture = false; this.playerControllerVisibility = Visibility.Hidden; this.playQuickIconVisibility = Visibility.Hidden; Logger.info(Constants.SEEKING_END); } onActionFunc: () => void = () => { this.playerControllerTransientDisplay(); } ... } ``` > 注意:这里的this.xxx属性都与GetFromInternet组件中的同名属性通过@Link双向绑定,属性说明可前往8.3.5.1中查看,此处不做冗余介绍。 `onActionStartFunc`代码逻辑:当触发平移手势时,将isSeekingGesture设定为true,此变量用于判断是否处在平移手势触发过程中,在后续介绍的进度条更新回调中有所体现,如果处在触发过程,则停止计算当前视频播放时长,即保证在快进或快退中进度条处于停滞状态。使进度控制板和快进/快退图标显现,并记录当前的偏移量。 `onActionUpdateFunc`代码逻辑:当处在平移过程中时,根据偏移量差值是否为负,来判断当前是快进还是快退状态。通过offsetX进行一定的转换计算,实时同步当前平移过程的视频时长变化,视觉上体现为进度条跟随手势平移在做移动,同时调用AVPlayer对象的seek()方法,将实际的视频流播放跳转到与进度条对应的位置。 `onActionEndFunc`代码逻辑:执行与`onActionStartFunc`相对的操作。isSeekingGesture设为false,进度控制板和快进/快退图标隐藏,也可以尝试使用setTimeout(js内置的方法)使控制板维持一段时间再隐藏,更加符合日常使用的播放器的逻辑。 `onActionFunc`代码逻辑:当触发点击手势时,调用在GetFromInternet组件中定义的playerControllerTransientDisplay()方法,使得播放控制组件(播放/暂停按键、进度条等)短暂显示,设定时间为3秒。 > ```TypeScript > // GetFromInternet.ets > > @Entry > @Component > struct GetFromInternet { > ... > > /** > * 播放控制组件(播放/暂停按键、进度条等)的短暂显示 > */ > playerControllerTransientDisplay() { > this.playerControllerVisibility = Visibility.Visible; > setTimeout(() => { > this.playerControllerVisibility = Visibility.Hidden; > }, Constants.TRANSIENT_DISPLAY_TIME); > } > > ... > } > ``` 3. 进度条时长显示 通过两个Text组件和一个Progress组件构成进度条板块。其中第一个Text中的内容随着curValue的改变而改变,通过格式化时间算法以“xx:xx”的形式呈现出来;第二个Text中的内容虽然每次进度条更新的时候都会去重新赋值,但是只要播放资源不变,获取的总视频时长不会有更改,所以第二个Text中的内容不变,显示为视频的总长;Progress组件则随curValue实时的变化而实时更新显示,与日常所见的播放器进度条一致。 ```TypeScript // PlayControl.ets class Package { curTime: string = Constants.DEFAULT_STRING; totalTime: string = Constants.DEFAULT_STRING; curValue: number = Constants.INIT_NUM; playerControllerVisibility: Visibility = Visibility.None; } @Builder function progressModule($$: Package) { Row({ space: SPACE }) { Text($$.curTime) .fontSize(FontSize.TIME) .fontColor(Color.FONT_WHITE) Progress({ value: $$.curValue, type: ProgressType.Linear }) .width(Width.PROGRESS_BAR) .color(Color.FONT_WHITE) Text($$.totalTime) .fontSize(FontSize.TIME) .fontColor(Color.FONT_WHITE) } .progress($$.playerControllerVisibility) } ``` > Package类的属性值与GetFromInternet中的属性名称和功能一一对应,使用progressModule自定义函数的时候传入的是与GetFromInternet相关属性双向绑定的值。 对curTime和curValue的处理都在GetFromInternet中定义的进度条更新回调函数updateProgressBarCallback中,用于自定义的UPDATE_PROGRESS_BAR_EVENT事件的注册,以下是这个函数的代码实现: ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... /** * 进度条更新回调 */ private updateProgressBarCallback: () => void = () => { if (this.videoPlayer) this.totalDuration = this.videoPlayer.getDuration(); this.totalTime = TimeUtil.format(this.totalDuration); if (this.videoPlayer) this.curDuration = this.videoPlayer.getCurDuration(); this.curTime = TimeUtil.format(this.curDuration); if (!this.isSeekingGesture) { this.curValue = (this.curDuration / this.totalDuration) * Constants.PERCENTAGE_MULTI; } else { this.preValue = this.curValue; } } ... } ``` 代码解读:格式化当前视频时长和视频总时长,存储进curTime和totalTime属性中用于对应的UI刷新。如果当前没有平移手势触发,则实时计算curValue值对进度条进行反馈;如果有,则存储移动前的时长进preValue,用于计算移动的距离,更新进度条。 格式化时间函数定义如下:实现的是将毫秒数更改为"xx:xx"格式字符串进行返回。 ```TypeScript // TimeUtils.ets export default class TimeUtil { /** * 格式化获取到的时间 * @param millisecond 输入毫秒数返回 "00:00:00"格式字符串 */ static format(millisecond: number) { const totalSeconds = Math.floor(millisecond / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const timeParts = [hours, minutes, seconds] .map(unit => unit.toString().padStart(2, '0')); // 保证每个时间单位都是两位数 // 如果没有小时,则只返回分钟和秒 return hours === 0 ? timeParts.slice(1).join(':') : timeParts.join(':'); } } ``` ##### 视频播放状态控制台 仅介绍“开发者模式”。 ![1722325704128-6](./README.assets/1722325704128-6.png) 1. 与播放状态相关的自定义事件的回调 以下是对PLAYER_PLAY_SUCCESS_EVENT、PLAYER_PAUSE_SUCCESS_EVENT和PLAYER_COMPLETE_SUCCESS_EVENT这三个事件注册的回调函数的说明,这三个自定义事件主要用于对状态响应后的部分UI展示进行控制,在这里表现为对播放/暂停图标的切换和进度控制板的显隐控制。 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... /** * 成功播放回调 */ private playerPlaySuccessCallback: () => void = () => { this.playIcon = Constants.PLAYER_PAUSE_ICON; this.playerControllerTransientDisplay(); } /** * 暂停播放回调 */ private playerPauseSuccessCallback: () => void = () => { this.playIcon = Constants.PLAYER_PLAY_ICON; this.playerControllerTransientDisplay(); } /** * 播放结束回调 */ private playerCompleteSuccessCallback: () => void = () => { this.showToast(Constants.VIDEO_COMPLETED); this.playerControllerTransientDisplay(); } ... } ``` 2. 与控制台七个按钮绑定的点击回调函数共有部分的抽象 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... /** * 开发者模式下各个按钮绑定的回调函数 * @param desState 目标到达的状态 * @param operaName 当前的操作名称 * @param func 满足条件最本质要调用的方法 * @param self 调用的方法处于的类对象,将自身传入,以解决this指向不明确的问题 * @param curStates 能够成功调用目标方法的几种当前状态 */ async onClickFunc(desState: string, operaName: string, func: (self: MediaPlayer) => Promise, self: MediaPlayer, ...curStates: Array) { if (this.videoPlayer) { let condition: boolean = false; for (let curState of curStates) { condition = condition || this.videoPlayer.getState() == curState; } if (this.videoPlayer && (condition)) { await func(self); } else if (this.videoPlayer && this.videoPlayer.getState() == desState) { this.showToast('当前已经是' + desState); } else if (this.videoPlayer.getState()) { this.showToast(`当前为${this.videoPlayer.getState()},拒绝` + operaName + `操作`); if (this.videoPlayer.getState() == Constants.AVPLAYER_STATE_RELEASED) { setTimeout(() => { this.showToast(Constants.HINT_AFTER_RETENTION); }, Constants.RESIDENCE_TIME); } } } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } ... } ``` 代码解读:首先对当前输入的能够成功调用目标方法的几种状态进行拼装,使其能够成为正确的判断条件。如果当前状态是这几种状态之一,则可以调用对应想要执行的操作;如果当前状态已经是执行想要执行的操作后会达到的状态,则弹窗提示用户“当前已经是xxx”;如果当前状态不符合上面两种情况,则代表不允许调用想要执行的操作,弹窗提示用户“当前为xxx,拒绝xxx操作”,假设当前状态还是released,则在弹窗结束后再弹出一个弹窗提示“当前已释放与此avPlayer关联的播放引擎,拒绝任何操作,请重新创建实例对象”。 3. 实现与各个按钮绑定的点击回调函数 + “初始化”按钮绑定GetFromInternet组件的initialize()方法 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... initialize: () => void = () => { if (this.videoPlayer) { if (this.text.includes(Constants.NETWORK_LINK_TAG_1) || this.text.includes(Constants.NETWORK_LINK_TAG_2)) { this.videoPlayer.prepareForVideo(this.text, this.xComponentController.getXComponentSurfaceId()); this.onClickFunc(Constants.AVPLAYER_STATE_INITIALIZED, Constants.INITIALIZE, this.videoPlayer.initialize, this.videoPlayer, Constants.AVPLAYER_STATE_IDLE); } else { this.showToast(Constants.URL_IS_WRONG); } } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } ... } ``` 代码解读:校验当前存储的url地址,如果包含“http://”和“https://”代表是一个网络地址,则调用videoPlayer的prepareForVideo()方法,将url和surfaceId存储进videoPlayer的属性中,等待需要的时候使用,然后调用onClickFunc进而调用在videoPlayer中定义的initialize()方法进行初始化。 + “加载准备”按钮绑定GetFromInternet组件的prepare()方法 ```TypeScript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... prepare: () => void = () => { if (this.videoPlayer) { this.showToast(Constants.PRE_PREPARE); // 由于prepare()接口异步操作时间较长,用来提示用户并非出现异常 this.onClickFunc(Constants.AVPLAYER_STATE_PREPARED, Constants.PREPARE, this.videoPlayer.prepare, this.videoPlayer, Constants.AVPLAYER_STATE_INITIALIZED, Constants.AVPLAYER_STATE_STOPPED); } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } ... } ``` 代码解读:因为AVPlayer对象的prepare()异步执行的时间较长,所以先弹窗提示用户耐心等待,然后调用onClickFunc进而调用在videoPlayer中定义的prepare()方法准备视频资源。 + “开始播放”按钮绑定GetFromInternet组件的play()方法,“暂停视频”按钮绑定GetFromInternet组件的pause()方法,“终止播放”按钮绑定GetFromInternet组件的stop()方法,“重置资源”按钮绑定GetFromInternet组件的reset()方法,“销毁实例”按钮绑定GetFromInternet组件的release()方法 ```typescript // GetFromInternet.ets @Entry @Component struct GetFromInternet { ... play: () => void = () => { if (this.videoPlayer) { this.onClickFunc(Constants.AVPLAYER_STATE_PLAYING, Constants.PLAY, this.videoPlayer.play, this.videoPlayer, Constants.AVPLAYER_STATE_PREPARED, Constants.AVPLAYER_STATE_PAUSED, Constants.AVPLAYER_STATE_COMPLETED); } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } pause: () => void = () => { if (this.videoPlayer) { this.onClickFunc(Constants.AVPLAYER_STATE_PAUSED, Constants.PAUSE, this.videoPlayer.pause, this.videoPlayer, Constants.AVPLAYER_STATE_PLAYING); } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } stop: () => void = () => { if (this.videoPlayer) { this.onClickFunc(Constants.AVPLAYER_STATE_STOPPED, Constants.STOP, this.videoPlayer.stop, this.videoPlayer, Constants.AVPLAYER_STATE_PREPARED, Constants.AVPLAYER_STATE_PLAYING, Constants.AVPLAYER_STATE_PAUSED, Constants.AVPLAYER_STATE_COMPLETED); } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } reset: () => void = () => { if (this.videoPlayer) { this.onClickFunc(Constants.AVPLAYER_STATE_IDLE, Constants.RESET, this.videoPlayer.reset, this.videoPlayer, Constants.AVPLAYER_STATE_INITIALIZED, Constants.AVPLAYER_STATE_PREPARED, Constants.AVPLAYER_STATE_PLAYING, Constants.AVPLAYER_STATE_PAUSED, Constants.AVPLAYER_STATE_COMPLETED, Constants.AVPLAYER_STATE_STOPPED, Constants.AVPLAYER_STATE_ERROR); } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } release: () => void = () => { if (this.videoPlayer) { this.onClickFunc(Constants.AVPLAYER_STATE_RELEASED, Constants.RELEASE, this.videoPlayer.release, this.videoPlayer, Constants.AVPLAYER_STATE_IDLE, Constants.AVPLAYER_STATE_INITIALIZED, Constants.AVPLAYER_STATE_PREPARED, Constants.AVPLAYER_STATE_PLAYING, Constants.AVPLAYER_STATE_PAUSED, Constants.AVPLAYER_STATE_COMPLETED, Constants.AVPLAYER_STATE_STOPPED, Constants.AVPLAYER_STATE_ERROR); } else { Logger.error(Constants.MEDIA_PLAYER_IS_NULL); } } ... } ``` #### 8.3.5.3 完整逻辑 用户进入应用后,点击“从网络获取”模块进入到GetFromInternet页面。此时创建GetFromInternet组件会为四个自定义的事件注册相关的回调函数,创建GetFromInternet组件后显示页面,为GetFromInternet的videoPlayer属性new一个MediaPlayer类的实例,并且执行一定初始化操作,使得videoPlayer的avPlayer属性成功与一个播放引擎挂钩,为videoPlayer的avPlayer属性中的一些事件注册了相关的回调函数,然后短暂显示进度控制板。当用户点击下拉图标时,GetFromInternet的isPullDown属性改为true,体现为刷新UI以显示下拉框。选中下拉框中的第一个资源内容,GetFromInternet的isSelect_1属性发生更改,体现为被选中内容底色变为紫色并且回收下拉框,输入框中出现选中的内容,并且将内容同步到GetFromInternet的text属性。点击“获取”按钮进行校验。点击“开发者调试”,GetFromInternet的isUser属性发生更改,呈现出开发者的控制台。点击“初始化”按钮,会调用到videoPlayer的initialize()方法,配置videoPlayer的avPlayer属性的url和surfaceId,videoPlayer的avPlayer进入到“initialized”状态。点击“加载准备”按钮,弹窗提示用户“请稍等片刻”,会调用到videoPlayer的prepare()方法,里面又调用到videoPlayer的avPlayer属性的prepare()方法,等待异步执行的完成,videoPlayer的avPlayer进入到“prepared”状态,并且弹窗提示用户当前的状态,供用户确认操作是否成功。后面“开始播放”、“暂停视频”等按钮的实现逻辑与“加载准备”按钮相差不大。当videoPlayer的avPlayer为“playing”状态,"timeUpdate"事件会实时上报当前时间,在此事件注册的回调函数中将UPDATE_PROGRESS_BAR_EVENT进行推送,所以也会触发UPDATE_PROGRESS_BAR_EVENT事件注册的回调函数,表现为进度条的实时刷新。在每一次状态切换时,"stateChange"会上报,并传递当前的状态作为携带数据给到注册的回调函数,当状态为“palying”、“paused”、“completed”时会对应推送PLAYER_PLAY_SUCCESS_EVENT、PLAYER_PAUSE_SUCCESS_EVENT、PLAYER_COMPLETE_SUCCESS_EVENT事件,触发回调函数导致进度控制板的短暂出现。点击“返回”图标,将会回到Home页面,页面隐藏后,调用videoPlayer的release()方法,取消对videoPlayer的avPlayer属性中的一些事件的订阅,并释放与videoPlayer的avPlayer属性关联的播放引擎,GetFromInternet组件销毁时取消对四个自定义事件的订阅。 ## 8.4 案例效果展示 请前往`./README.assets/案例效果展示.mp4`中查看。 ## 8.5 小结 本章讲述了关于AVPlayer类的使用,介绍了视频播放中的几种状态和切换状态的方法。通过一个从网络获取视频进行播放的案例带领读者初步体悟媒体开发,在实战演练加深对 AVPlayer 类和事件管理机制的认识,并在其中穿插讲解了emitter的基本使用和为组件绑定手势操作的方法。通过本章的学习,读者应该能够独立进行基本的播放开发,为进一步深入媒体服务开发打下基础。没有任何讲解能够替代源码,通过完整的代码文件动手一步步去复现才能够更加深入体会对视频播放各个流程的把控以及封装的思想。 通过以下仓库读者可自行获取完整代码: [简易视频播放器~(gitee.com)](https://gitee.com/openharmonie-e/avplayer) ## 8.6 课后习题 1. AVPlayer对象分别能够处于哪些状态?各个状态之间是如何做到切换的? 2. [多选]以下on('error')和error状态的说法正确的是() ​ A. 触发on('error')事件代表着进入了error状态。 ​ B. 进入error状态一定会触发on('error')事件。 ​ C. 可以在未进入error状态时收到on('error')。 ​ D. 进入error状态的详细错误信息可以通过on('error')来获取。 ​ 答案:BCD。 3. 尝试去了解emitter的上报优先级,明确上报事件和触发事件背后的顺序逻辑。 4. 仿照着视频播放的开发,增加音频播放的功能模块。(提示:不用设置surfaceId,其余操作不变) 5. 在AVPlayer对象中,还存在许多的事件订阅函数,请选择合适的方法为视频播放器增加功能,比如音量调控、亮度调控、倍速播放等。 6. 结合第六章的内容,完善从文件获取视频播放的功能模块。 7. 结合第七章的内容,实现播放自己录制的视频的功能。