鸿蒙实战开发:视频播放器实现(中)
在上篇文章中,我们实现了视频播放器的一些基本设置,在本文中,我们继续讲解视频播放器播放功能的实现。
1、维护播放列表和播放索引
我们实现的播放器,其核心思想是维护一个播放列表和当前的播放索引,当播放某个视频时,通过播放索引去切换当前要播放的视频。因此在播放器类中,需要定义playList变量和playIndex变量分别存储播放列表和当前的播放索引。
import media from '@ohos.multimedia.media'
export class VideoAVPlayerClass {
// 创建的播放器应该存在我们的工具类上,这样才能被导出使用
static player: media.AVPlayer | null = null
// 当前播放器播放视频的总时长
static duration: number = 0
// 当前播放器播放的时长
static time: number = 0
// 当前播放器是否播放
static isPlay: boolean = false
// 当前播放器的播放列表
static playList: videoItemType[] = []
// 当前播放的视频索引
static playIndex: number = 0
// surfaceID用于播放画面显示,具体的值需要通过XComponent接口获取
static surfaceId: string = ''
// 创建播放器的方法
static async init(initParams: InitParams) {
// 存储属性SurfaceID,用于设置播放窗口,显示画面
VideoAVPlayerClass.surfaceId = initParams.surfaceId
// 创建播放器实例
VideoAVPlayerClass.player = await media.createAVPlayer()
// ----------------------- 事件监听 --------------------------------------------------------------
// 用于进度条,监听进度条长度,刷新资源时长
VideoAVPlayerClass.avPlayer.on('durationUpdate', (duration: number) => {
console.info('AVPlayer state durationUpdate called. current time: ', duration);
// 获取视频总时长
VideoAVPlayerClass.duration = duration
})
// 用于进度条,监听进度条当前位置,刷新当前时间
VideoAVPlayerClass.avPlayer.on('timeUpdate', (time) =>{
console.info('AVPlayer state timeUpdate called. current time: ', time);
// 获取当前播放时长
VideoAVPlayerClass.time = time
})
// 监听seek生效的事件
VideoAVPlayerClass.avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
VideoAVPlayerClass.avPlayer.play()
VideoAVPlayerClass.isPlay = true
})
// 监听视频播放错误事件,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
VideoAVPlayerClass.avPlayer.on('error', (err) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
// 调用reset重置资源,触发idle状态
VideoAVPlayerClass.avPlayer.reset()
})
// 监听播放状态机AVPlayerState切换的事件
VideoAVPlayerClass.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
switch (state) {
// 成功调用reset接口后触发该状态机上报
case 'idle':
console.info('AVPlayer state idle called.');
break
// avplayer 设置播放源后触发该状态上报
case 'initialized':
console.info('AVPlayerstate initialized called.');
// 设置显示画面,当播放的资源为纯音频时无需设置
VideoAVPlayerClass.avPlayer.surfaceId = VideoAVPlayerClass.surfaceId
break
// prepare调用成功后上报该状态机
case 'prepared':
console.info('AVPlayer state prepared called.');
break
// play成功调用后触发该状态机上报
case 'playing':
console.info('AVPlayer state playing called.');
break
// pause成功调用后触发该状态机上报
case 'paused':
console.info('AVPlayer state paused called.');
break
// 播放结束后触发该状态机上报
case 'completed':
console.info('AVPlayer state completed called.');
break
// stop接口成功调用后触发该状态机上报
case 'stopped':
console.info('AVPlayer state stopped called.');
// 调用reset接口初始化avplayer状态
VideoAVPlayerClass.avPlayer.reset()
break
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
}
static async changePlay() {
// 将播放状态置为闲置
await VideoAVPlayerClass.avPlayer.reset()
VideoAVPlayerClass.avPlayer.url = VideoAVPlayerClass.playList[VideoAVPlayerClass.playIndex].url
}
}
2、更新信息到页面
我们已经记录了页面需要的播放信息,视频也已经能正常播放了,但是页面还没有更新,我们如何将这些信息同步到页面呢?
在我们的实现中,是工具类到Page的通信,因此需要使用到线程通信。线程通信通常有Emitter 和 Worker 两种方式。Emitter适用于线程间发送和处理事件的能力(发布订阅),Worker适用于与主线程并行的独立线程(并行计算),这里更适合使用Emitter来进行通信,从而传递播放信息至各页面。其中,发布者是播放器,订阅者就是需要使用信息的页面。
- 发布事件
首先,我们在播放器类中定义一个updateState方法,用于更新页面状态。在该方法中调用emitter.emit() 方法来发布事件,然后分别在timeUpdate事件监听和changePlay()方法中调用updateState()方法:
import media from '@ohos.multimedia.media'
export class VideoAVPlayerClass {
// 创建的播放器应该存在我们的工具类上,这样才能被导出使用
static player: media.AVPlayer | null = null
// 当前播放器播放视频的总时长
static duration: number = 0
// 当前播放器播放的时长
static time: number = 0
// 当前播放器是否播放
static isPlay: boolean = false
// 当前播放器的播放列表
static playList: videoItemType[] = []
// 当前播放的视频索引
static playIndex: number = 0
// surfaceID用于播放画面显示,具体的值需要通过XComponent接口获取
static surfaceId: string = ''
// 创建播放器的方法
static async init(initParams: InitParams) {
// 存储属性SurfaceID,用于设置播放窗口,显示画面
VideoAVPlayerClass.surfaceId = initParams.surfaceId
// 创建播放器实例
VideoAVPlayerClass.player = await media.createAVPlayer()
// ----------------------- 事件监听 --------------------------------------------------------------
// 用于进度条,监听进度条长度,刷新资源时长
VideoAVPlayerClass.avPlayer.on('durationUpdate', (duration: number) => {
console.info('AVPlayer state durationUpdate called. current time: ', duration);
// 获取视频总时长
VideoAVPlayerClass.duration = duration
})
// 用于进度条,监听进度条当前位置,刷新当前时间
VideoAVPlayerClass.avPlayer.on('timeUpdate', (time) =>{
console.info('AVPlayer state timeUpdate called. current time: ', time);
// 获取当前播放时长
VideoAVPlayerClass.time = time
// 更新信息到页面
VideoAVPlayerClass.updateState()
})
// 监听seek生效的事件
VideoAVPlayerClass.avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
VideoAVPlayerClass.avPlayer.play()
VideoAVPlayerClass.isPlay = true
})
// 监听视频播放错误事件,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
VideoAVPlayerClass.avPlayer.on('error', (err) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
// 调用reset重置资源,触发idle状态
VideoAVPlayerClass.avPlayer.reset()
})
// 监听播放状态机AVPlayerState切换的事件
VideoAVPlayerClass.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
switch (state) {
// 成功调用reset接口后触发该状态机上报
case 'idle':
console.info('AVPlayer state idle called.');
break
// avplayer 设置播放源后触发该状态上报
case 'initialized':
console.info('AVPlayerstate initialized called.');
// 设置显示画面,当播放的资源为纯音频时无需设置
VideoAVPlayerClass.avPlayer.surfaceId = VideoAVPlayerClass.surfaceId
break
// prepare调用成功后上报该状态机
case 'prepared':
console.info('AVPlayer state prepared called.');
break
// play成功调用后触发该状态机上报
case 'playing':
console.info('AVPlayer state playing called.');
break
// pause成功调用后触发该状态机上报
case 'paused':
console.info('AVPlayer state paused called.');
break
// 播放结束后触发该状态机上报
case 'completed':
console.info('AVPlayer state completed called.');
break
// stop接口成功调用后触发该状态机上报
case 'stopped':
console.info('AVPlayer state stopped called.');
// 调用reset接口初始化avplayer状态
VideoAVPlayerClass.avPlayer.reset()
break
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
}
static async changePlay() {
// 将播放状态置为闲置
await VideoAVPlayerClass.avPlayer.reset()
VideoAVPlayerClass.avPlayer.url = VideoAVPlayerClass.playList[VideoAVPlayerClass.playIndex].url
VideoAVPlayerClass.updateState()
}
// 更新页面状态
static async updateState() {
const data = {
playState: JSON.stringify({
duration: VideoAVPlayerClass.duration,
time: VideoAVPlayerClass.time,
isPlay: VideoAVPlayerClass.isPlay,
playIndex: VideoAVPlayerClass.playIndex,
playList: VideoAVPlayerClass.playList,
})
}
// 更新页面
emitter.emit({
eventId: EmitEventType.UPDATE_STATE
}, {
data
})
}
}
- 订阅事件
由于需要记录播放状态,所以我们需要声明playState
变量,并且页面需要根据这个数据进行更新,所以需要使用@State
修饰符。当页面订阅到最新数据,就会将数据保存到playState变量中,playState的变化会触发页面重新渲染。
import emitter from '@ohos.events.emitter';
import { EmitEventType } from '../constants/EventContants';
import { VideoListData } from '../constants/VideoConstants';
import { PlayStateType, PlayStateTypeModel } from '../models/playState';
import { videoItemType } from '../models/video';
import { VideoPlayStateType, VideoPlayStateTypeModel } from '../models/videoPlayState';
@Preview
@Component
struct Index {
@State
playState: VideoPlayStateType = new VideoPlayStateTypeModel({} as VideoPlayStateType)
xComController: XComponentController = new XComponentController()
surfaceId: string = "" // 定义surfaceId
videoList: videoItemType[] = VideoListData
async aboutToAppear() {
// 从播放器订阅数据
emitter.on({ eventId: EmitEventType.UPDATE_STATE }, (data) => {
this.playState = new VideoPlayStateTypeModel(JSON.parse(data.data.playState))
})
}
build() {
Row() {
Column({ space: 10 }) {
Stack() {
Column() {
Row(){
// 视频播放窗口
XComponent({
id: 'videoXComponent',
type: 'surface',
controller: this.xComController
})
.width('100%')
.height(200)
.onLoad(async () => {
this.xComController.setXComponentSurfaceSize({ surfaceWidth: 1080, surfaceHeight: 1920 });
this.surfaceId = this.xComController.getXComponentSurfaceId()
})
}
}
.width('100%')
.height(270)
.padding({
top: 30,
bottom:30
})
.backgroundColor($r('app.color.black'))
.justifyContent(FlexAlign.Start)
}
}
.width('100%')
.height('100%')
}
.height('100%')
.width('100%')
}
}
export default Video_Play
- 展示播放信息
在页面中,根据播放信息,展示视频时长,当前播放时长,根据是否播放状态展示暂停/播放按钮。
import emitter from '@ohos.events.emitter';
import PlayingAnimation from '../components/PlayingAnimation';
import { EmitEventType } from '../constants/EventContants';
import { VideoListData } from '../constants/VideoConstants';
import { PlayStateType, PlayStateTypeModel } from '../models/playState';
import { videoItemType } from '../models/video';
import { VideoPlayStateType, VideoPlayStateTypeModel } from '../models/videoPlayState';
@Preview
@Component
struct Index {
@State
playState: VideoPlayStateType = new VideoPlayStateTypeModel({} as VideoPlayStateType)
xComController: XComponentController = new XComponentController()
surfaceId: string = "" // 定义surfaceId
videoList: videoItemType[] = VideoListData
async aboutToAppear() {
// 从播放器订阅数据
emitter.on({ eventId: EmitEventType.UPDATE_STATE }, (data) => {
this.playState = new VideoPlayStateTypeModel(JSON.parse(data.data.playState))
})
}
// 时长数字(ms)转字符串
number2time(number: number) {
if (!number) {
return '00:00'
}
const ms: number = number % 1000
const second = (number - ms) / 1000
const s: number = second % 60
if (second > 60) {
const m: number = (second - s) / 60 % 60
return m.toString()
.padStart(2, '0') + ':' + s.toString()
.padStart(2, '0')
}
return '00:' + s.toString()
.padStart(2, '0')
}
build() {
Row() {
Column({ space: 10 }) {
Stack() {
Column() {
Row(){
// 视频播放窗口
XComponent({
id: 'videoXComponent',
type: 'surface',
controller: this.xComController
})
.width('100%')
.height(200)
.onLoad(async () => {
this.xComController.setXComponentSurfaceSize({ surfaceWidth: 1080, surfaceHeight: 1920 });
this.surfaceId = this.xComController.getXComponentSurfaceId()
})
}
// 进度条
Row({space: 6}){
// 当前播放时长
Text(this.number2time(this.playState?.time))
.fontColor($r('app.color.white'))
.visibility(this.playState?.duration ? Visibility.Visible : Visibility.Hidden)
// 进度条
Slider({
value: this.playState.time,
min: 0,
max: this.playState.duration,
})
.trackColor($r('app.color.white'))
.width("70%")
// 视频总时长
Text(this.number2time(this.playState?.duration))
.fontColor($r('app.color.white'))
.visibility(this.playState?.duration ? Visibility.Visible : Visibility.Hidden)
}
.width('100%')
.height(20)
.margin({
top: 10
})
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height(270)
.padding({
top: 30,
bottom:30
})
.backgroundColor($r('app.color.black'))
.justifyContent(FlexAlign.Start)
// 播放按钮
if (!this.playState.isPlay) {
Image($r('app.media.ic_play'))
.width(48)
.height(48)
.fillColor($r('app.color.white'))
}
}
// 视频列表缩略图
List({ space: 10, initialIndex: 0 }) {
ForEach(this.videoList, (item: videoItemType, index: number) => {
ListItem() {
Stack({alignContent: Alignment.Center}){
Image(item.imgUrl)
.width(100)
.height(80)
// .objectFit(ImageFit.Contain)
if (this.playState.playIndex === index) {
Row(){
PlayingAnimation({ recordIng: true })
}
}
}
}
.width(100)
}, item => item)
}
.height(100)
.listDirection(Axis.Horizontal) // 排列方向
.edgeEffect(EdgeEffect.Spring) // 滑动到边缘无效果
.onScrollIndex((firstIndex: number, lastIndex: number) => {
console.info('first' + firstIndex)
console.info('last' + lastIndex)
})
}
.width('100%')
.height('100%')
}
.height('100%')
.width('100%')
}
}
export default Index
3、播放视频
播放窗口,播放源都已经设置好了,播放列表也有了,我们就可以播放视频了。在页面中调用播放器的init()方法,传入从XComponent组件获取的surfaceId和视频列表,然后调用播放器的singlePlay()方法,播放器就会根据当前的播放索引去播放列表中的视频。
import emitter from '@ohos.events.emitter';
import PlayingAnimation from '../components/PlayingAnimation';
import { EmitEventType } from '../constants/EventContants';
import { VideoListData } from '../constants/VideoConstants';
import { PlayStateType, PlayStateTypeModel } from '../models/playState';
import { videoItemType } from '../models/video';
import { VideoPlayStateType, VideoPlayStateTypeModel } from '../models/videoPlayState';
import { VideoAVPlayerClass } from '../utils/VideoAVPlayerClass';
@Preview
@Component
struct Index {
@State
playState: VideoPlayStateType = new VideoPlayStateTypeModel({} as VideoPlayStateType)
xComController: XComponentController = new XComponentController()
surfaceId: string = "" // 定义surfaceId
videoList: videoItemType[] = VideoListData
async aboutToAppear() {
// 从播放器订阅数据
emitter.on({ eventId: EmitEventType.UPDATE_STATE }, (data) => {
this.playState = new VideoPlayStateTypeModel(JSON.parse(data.data.playState))
})
}
aboutToDisappear(){
// 销毁播放器
VideoAVPlayerClass.avPlayer.release()
}
// 时长数字(ms)转字符串
number2time(number: number) {
if (!number) {
return '00:00'
}
const ms: number = number % 1000
const second = (number - ms) / 1000
const s: number = second % 60
if (second > 60) {
const m: number = (second - s) / 60 % 60
return m.toString()
.padStart(2, '0') + ':' + s.toString()
.padStart(2, '0')
}
return '00:' + s.toString()
.padStart(2, '0')
}
build() {
Row() {
Column({ space: 10 }) {
Stack() {
Column() {
Row(){
// 视频播放窗口
XComponent({
id: 'videoXComponent',
type: 'surface',
controller: this.xComController
})
.width('100%')
.height(200)
.onLoad(async () => {
this.xComController.setXComponentSurfaceSize({ surfaceWidth: 1080, surfaceHeight: 1920 });
this.surfaceId = this.xComController.getXComponentSurfaceId()
if (this.surfaceId) {
await VideoAVPlayerClass.init({surfaceId: this.surfaceId, playList: this.videoList, context: getContext(this)})
await VideoAVPlayerClass.singlePlay()
}
})
}
// 进度条
Row({space: 6}){
// 当前播放时长
Text(this.number2time(this.playState?.time))
.fontColor($r('app.color.white'))
.visibility(this.playState?.duration ? Visibility.Visible : Visibility.Hidden)
// 进度条
Slider({
value: this.playState.time,
min: 0,
max: this.playState.duration,
})
.trackColor($r('app.color.white'))
.width("70%")
// 视频总时长
Text(this.number2time(this.playState?.duration))
.fontColor($r('app.color.white'))
.visibility(this.playState?.duration ? Visibility.Visible : Visibility.Hidden)
}
.width('100%')
.height(20)
.margin({
top: 10
})
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height(270)
.padding({
top: 30,
bottom:30
})
.backgroundColor($r('app.color.black'))
.justifyContent(FlexAlign.Start)
// 播放按钮
if (!this.playState.isPlay) {
Image($r('app.media.ic_play'))
.width(48)
.height(48)
.fillColor($r('app.color.white'))
}
}
// 视频列表缩略图
List({ space: 10, initialIndex: 0 }) {
ForEach(this.videoList, (item: videoItemType, index: number) => {
ListItem() {
Stack({alignContent: Alignment.Center}){
Image(item.imgUrl)
.width(100)
.height(80)
// .objectFit(ImageFit.Contain)
if (this.playState.playIndex === index) {
Row(){
PlayingAnimation({ recordIng: true })
}
}
}
}
.width(100)
}, item => item)
}
.height(100)
.listDirection(Axis.Horizontal) // 排列方向
.edgeEffect(EdgeEffect.Spring) // 滑动到边缘无效果
.onScrollIndex((firstIndex: number, lastIndex: number) => {
console.info('first' + firstIndex)
console.info('last' + lastIndex)
})
}
.width('100%')
.height('100%')
}
.height('100%')
.width('100%')
}
}
export default Index
至此,我们实现的播放器可以播放视频了,但是它还不能暂停,也不能切换进度,切换视频,这些功能,将在下一篇分享
最后
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。
这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。
希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!
如果你是一名有经验的资深Android移动开发、Java开发、前端开发、对鸿蒙感兴趣以及转行人员,可以直接领取这份资料
获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
鸿蒙(HarmonyOS NEXT)最新学习路线
-
HarmonOS基础技能
- HarmonOS就业必备技能
- HarmonOS多媒体技术
- 鸿蒙NaPi组件进阶
- HarmonOS高级技能
- 初识HarmonOS内核
- 实战就业级设备开发
有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。
获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
《鸿蒙 (OpenHarmony)开发入门教学视频》
《鸿蒙生态应用开发V2.0白皮书》
《鸿蒙 (OpenHarmony)开发基础到实战手册》
OpenHarmony北向、南向开发环境搭建
《鸿蒙开发基础》
- ArkTS语言
- 安装DevEco Studio
- 运用你的第一个ArkTS应用
- ArkUI声明式UI开发
- .……
《鸿蒙开发进阶》
- Stage模型入门
- 网络管理
- 数据管理
- 电话服务
- 分布式应用开发
- 通知与窗口管理
- 多媒体技术
- 安全技能
- 任务管理
- WebGL
- 国际化开发
- 应用测试
- DFX面向未来设计
- 鸿蒙系统移植和裁剪定制
- ……
《鸿蒙进阶实战》
- ArkTS实践
- UIAbility应用
- 网络案例
- ……
获取以上完整鸿蒙HarmonyOS学习资料,请点击→纯血版全套鸿蒙HarmonyOS学习资料
总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。