鸿蒙开发之视频播放器实现下篇

741 阅读10分钟

在《鸿蒙开发之视频播放器实现中篇》中,我们实现了视频播放器的播放功能,在本文中,我们继续讲解视频播放器暂停/播放、切换进度、切换视频功能的实现。

1、暂停/播放

目前已经可以播放视频了,但是并不能暂停,因此我们继续给播放器添加暂停/播放功能。

  • 暂停

给播放器添加一个 pause 的方法,在该方法调用AVPlayer的pause()来实现视频的暂停,同时将播放状态设置为 false,并通过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 pause() {
    VideoAVPlayerClass.avPlayer.pause()
    VideoAVPlayerClass.isPlay = false
    VideoAVPlayerClass.updateState()
  }

  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
    })
  }
  
}
  • 播放

给播放器添加一个 play 方法,在该方法中,在该方法调用AVPlayer的play()来实现视频的播放,同时将播放状态设置为 true,并通过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 play() {
    VideoAVPlayerClass.avPlayer.play()
    VideoAVPlayerClass.isPlay = true
    VideoAVPlayerClass.updateState()
  }

  // 视频暂停
  static pause() {
    VideoAVPlayerClass.avPlayer.pause()
    VideoAVPlayerClass.isPlay = false
    VideoAVPlayerClass.updateState()
  }

  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
    })
  }
  
}
  • 页面调用

在Index.ets页面中,根据播放状态,在点击播放窗口时调用 play() 或 pause() 方法进行视频的播放/暂停。

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()
                  }
                })
            }
            .onClick(() => {
              this.playState.isPlay ? VideoAVPlayerClass.pause() : VideoAVPlayerClass.play()
            })

            // 进度条
            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'))
              .onClick(() => {
                VideoAVPlayerClass.play()
              })
          }
        }

        // 视频列表缩略图
        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)
            .onClick(() => {
              VideoAVPlayerClass.singlePlay(item)
            })

          }, 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

2、切换播放进度

给播放器添加一个seekTime方法,指定播放进度。在该方法中,调用AVPlayer的 seek() 方法可以跳转到指定播放位置。

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 play() {
    VideoAVPlayerClass.avPlayer.play()
    VideoAVPlayerClass.isPlay = true
    VideoAVPlayerClass.updateState()
  }

  // 视频暂停
  static pause() {
    VideoAVPlayerClass.avPlayer.pause()
    VideoAVPlayerClass.isPlay = false
    VideoAVPlayerClass.updateState()
  }

    // 跳转到指定播放位置
  static seekTime(time: number) {
    VideoAVPlayerClass.time = time
    VideoAVPlayerClass.avPlayer.seek(VideoAVPlayerClass.time)

    // 如果是暂停状态下跳转到指定播放位置,则开启播放
    if (!VideoAVPlayerClass.isPlay) {
      VideoAVPlayerClass.isPlay = true
      VideoAVPlayerClass.avPlayer.play()
    }

  }

  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,
      })
    }
    // 更新页面
    emitter.emit({
      eventId: EmitEventType.UPDATE_STATE
    }, {
      data
    })
  }
  
}

在页面的进度条中调用seekTime(),实现播放进度的切换。

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()
                  }
                })
            }
            .onClick(() => {
              this.playState.isPlay ? VideoAVPlayerClass.pause() : VideoAVPlayerClass.play()
            })

            // 进度条
            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'))
                .onChange((value: number, mode: SliderChangeMode) => {
                  // 切换播放进度
                  VideoAVPlayerClass.seekTime(value)
                })
                .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'))
              .onClick(() => {
                VideoAVPlayerClass.play()
              })
          }
        }

        // 视频列表缩略图
        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、切换视频

  • 手动切换

当我们点击页面的视频列表中的某个视频时,切换到当前视频进行播放。我们在播放器类中添加一个singlePlay()方法来实现视频的切换。

在切换视频时,如果要切换的视频已经在播放列表中,则根据播放索引,播放当前切换的视频。如果要切换的视频不在播放列表中,就将当前切换的视频添加到播放列表中,然后播放当前切换的视频。

在切换视频前,我们还需要做一件事情,就是将当前播放时长和视频时长重置为0。

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 play() {
    VideoAVPlayerClass.avPlayer.play()
    VideoAVPlayerClass.isPlay = true
    VideoAVPlayerClass.updateState()
  }

  // 视频暂停
  static pause() {
    VideoAVPlayerClass.avPlayer.pause()
    VideoAVPlayerClass.isPlay = false
    VideoAVPlayerClass.updateState()
  }

  // 切换视频
  static singlePlay(video?: videoItemType) {

    if (video) {
      let index = VideoAVPlayerClass.playList.findIndex((item: videoItemType) => item.id === video.id)

      if (index > -1) {
        // 当前要播放的视频在播放列表里
        VideoAVPlayerClass.playIndex = index
      } else {
        // 当前要播放的视频不在播放列表里
        VideoAVPlayerClass.playList.push(video)
        VideoAVPlayerClass.playIndex = VideoAVPlayerClass.playList.length - 1
      }
    }

    VideoAVPlayerClass.changePlay()

  }

  static async changePlay() {
    // 将播放状态置为闲置
    await VideoAVPlayerClass.avPlayer.reset()

    // 重置当前播放时长和视频时长
    VideoAVPlayerClass.time = 0
    VideoAVPlayerClass.duration = 0

    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
    })
  }
  
}
  • 自动切换

当前视频播放完之后,自动播放下一个视频。监听播放器的stateChange事件,当播放器状态为completed状态时,获取下一个视频的索引,调用singlePlay()方法播放下一个视频。

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.');

          // 当前视频播放完成,自动播放下一个视频哦
          if (VideoAVPlayerClass.autoPlayList && VideoAVPlayerClass.playIndex < VideoAVPlayerClass.playList.length) {
            VideoAVPlayerClass.playIndex++
            VideoAVPlayerClass.playIndex = (VideoAVPlayerClass.playIndex + VideoAVPlayerClass.playList.length) % VideoAVPlayerClass.playList.length
            VideoAVPlayerClass.singlePlay(VideoAVPlayerClass.playList[VideoAVPlayerClass.playIndex])
            VideoAVPlayerClass.isPlay = true
          } else {
            VideoAVPlayerClass.isPlay = false
            // 停止播放
            VideoAVPlayerClass.avPlayer.stop()
          }
          
          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 play() {
    VideoAVPlayerClass.avPlayer.play()
    VideoAVPlayerClass.isPlay = true
    VideoAVPlayerClass.updateState()
  }

  // 视频暂停
  static pause() {
    VideoAVPlayerClass.avPlayer.pause()
    VideoAVPlayerClass.isPlay = false
    VideoAVPlayerClass.updateState()
  }

  // 切换视频
  static singlePlay(video?: videoItemType) {

    if (video) {
      let index = VideoAVPlayerClass.playList.findIndex((item: videoItemType) => item.id === video.id)

      if (index > -1) {
        // 当前要播放的视频在播放列表里
        VideoAVPlayerClass.playIndex = index
      } else {
        // 当前要播放的视频不在播放列表里
        VideoAVPlayerClass.playList.push(video)
        VideoAVPlayerClass.playIndex = VideoAVPlayerClass.playList.length - 1
      }
    }

    VideoAVPlayerClass.changePlay()

  }

  static async changePlay() {
    // 将播放状态置为闲置
    await VideoAVPlayerClass.avPlayer.reset()

    // 重置当前播放时长和视频时长
    VideoAVPlayerClass.time = 0
    VideoAVPlayerClass.duration = 0

    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
    })
  }
  
}
  • 页面调用

在页面的视频列表中调用播放器的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()
                  }
                })
            }
            .onClick(() => {
              this.playState.isPlay ? VideoAVPlayerClass.pause() : VideoAVPlayerClass.play()
            })

            // 进度条
            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'))
                .onChange((value: number, mode: SliderChangeMode) => {
                  // 切换播放进度
                  VideoAVPlayerClass.seekTime(value)
                })
                .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'))
              .onClick(() => {
                VideoAVPlayerClass.play()
              })
          }
        }

        // 视频列表缩略图
        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)
            .onClick(() => {
              // 切换视频
              VideoAVPlayerClass.singlePlay(item)
            })

          }, 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

4、缓存播放信息

我们的播放器已经可以正常进行视频的播放和切换了,当我们不小心退出了当前页面,再进入播放页面时,你会发现我们当前播放的视频信息没有了,这是为什么呢?

在当前的实现中,播放页面的信息是通过订阅播放器得到的,如果不播放了,就没有了信息来源的渠道,所以页面的播放信息就没有了。因此我们需要再建立一个信息收集渠道,即使不在播放时,也能获取到最后的播放信息数据。为此,我们使用**@ohos.data.preferences(用户首选项)**来持久化播放信息。

1、首先我们实现一个存储和读取首选项的工具类

import preferences from '@ohos.data.preferences'
import { videoDefaultState, VideoPlayStateType } from '../models/videoPlayState'

export class PreferencesClass {
  StoreName = 'VIDEO_PLAYER'
  context: Context

  VideoPlayStateKey = "VIDEO_PLAY_STATE"

  constructor(context: Context) {
    this.context = context
  }
  // 获取store
  async getStore() {
    return await preferences.getPreferences(this.context,this.StoreName)
  }

  // 存储视频播放状态
  async setVideoPlayState(playState:VideoPlayStateType){
    const store = await this.getStore()
    await store.put(this.PlayStateKey,JSON.stringify(playState))
    await store.flush()
  }
  // 读取视频播放状态
  async getVideoPlayState(): Promise<VideoPlayStateType> {
    const store = await this.getStore()
    return JSON.parse(await store.get(this.VideoPlayStateKey,JSON.stringify(videoDefaultState)) as string) as VideoPlayStateType

  }
}

2、播放器每次发布事件时,更新播放信息到首选项中,由于需要使用上下文,所以我们需要在初始化播放器的时候将上下文保存到播放器类中。需要注意的是,我们需要在页面中将上下文传给播放器。

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 context: Context|null = null
  
  
  // 创建播放器的方法
  static async init(initParams: InitParams) {

    // 存储属性SurfaceID,用于设置播放窗口,显示画面
    VideoAVPlayerClass.surfaceId = initParams.surfaceId

    // 将上下文 context 存储到播放器类上
    VideoAVPlayerClass.context = initParams.context

    // 创建播放器实例
    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.');

          // 当前视频播放完成,自动播放下一个视频哦
          if (VideoAVPlayerClass.autoPlayList && VideoAVPlayerClass.playIndex < VideoAVPlayerClass.playList.length) {
            VideoAVPlayerClass.playIndex++
            VideoAVPlayerClass.playIndex = (VideoAVPlayerClass.playIndex + VideoAVPlayerClass.playList.length) % VideoAVPlayerClass.playList.length
            VideoAVPlayerClass.singlePlay(VideoAVPlayerClass.playList[VideoAVPlayerClass.playIndex])
            VideoAVPlayerClass.isPlay = true
          } else {
            VideoAVPlayerClass.isPlay = false
            // 停止播放
            VideoAVPlayerClass.avPlayer.stop()
          }
          
          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 play() {
    VideoAVPlayerClass.avPlayer.play()
    VideoAVPlayerClass.isPlay = true
    VideoAVPlayerClass.updateState()
  }

  // 视频暂停
  static pause() {
    VideoAVPlayerClass.avPlayer.pause()
    VideoAVPlayerClass.isPlay = false
    VideoAVPlayerClass.updateState()
  }

  // 切换视频
  static singlePlay(video?: videoItemType) {

    if (video) {
      let index = VideoAVPlayerClass.playList.findIndex((item: videoItemType) => item.id === video.id)

      if (index > -1) {
        // 当前要播放的视频在播放列表里
        VideoAVPlayerClass.playIndex = index
      } else {
        // 当前要播放的视频不在播放列表里
        VideoAVPlayerClass.playList.push(video)
        VideoAVPlayerClass.playIndex = VideoAVPlayerClass.playList.length - 1
      }
    }

    VideoAVPlayerClass.changePlay()

  }

  static async changePlay() {
    // 将播放状态置为闲置
    await VideoAVPlayerClass.avPlayer.reset()

    // 重置当前播放时长和视频时长
    VideoAVPlayerClass.time = 0
    VideoAVPlayerClass.duration = 0

    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
    })
    
    // 存储首选项
    const preferences:PreferencesClass = new PreferencesClass(VideoAVPlayerClass.context)
    await preferences.setVideoPlayState(JSON.parse(data.playState))
    
  }
  
}

3、最后,在页面首次加载的时候,从首选项中读取播放信息,存储到playState中。

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 { PreferencesClass } from '../utils/PreferencesClass';
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))
    })

    // 从首选项加载数据
    const preferences:PreferencesClass = new PreferencesClass(getContext(this))
    this.playState = await preferences.getVideoPlayState()
  }

  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()
                  }
                })
            }
            .onClick(() => {
              this.playState.isPlay ? VideoAVPlayerClass.pause() : VideoAVPlayerClass.play()
            })

            // 进度条
            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'))
                .onChange((value: number, mode: SliderChangeMode) => {
                  // 切换播放进度
                  VideoAVPlayerClass.seekTime(value)
                })
                .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'))
              .onClick(() => {
                VideoAVPlayerClass.play()
              })
          }
        }

        // 视频列表缩略图
        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)
            .onClick(() => {
              VideoAVPlayerClass.singlePlay(item)
            })

          }, 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

此时,我们完成了播放信息的缓存,即使退出了播放页面,我们依然能通过首选项获取到播放信息。

至此,我们整个播放器的功能就算完成了🎉