开天辟地 HarmonyOS(鸿蒙) - 媒体: AVPlayer(播放器,用于播放视频或音频)

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

开天辟地 HarmonyOS(鸿蒙) - 媒体: AVPlayer(播放器,用于播放视频或音频)

示例如下:

pages\media\AVPlayerDemo.ets

/*
 * AVPlayer - 播放器(可以播放视频或音频)
 */

import { MyLog, TitleBar } from '../TitleBar'
import { BusinessError } from '@kit.BasicServicesKit'
import { media } from '@kit.MediaKit'
import { LengthMetrics } from '@kit.ArkUI'
import { fileIo as fs } from '@kit.CoreFileKit';

@Entry
@Component
struct AVPlayerDemo {

  build() {
    Column({ space: 10 }) {
      TitleBar()
      Tabs() {
        TabContent() { MySample1() }.tabBar('基础').align(Alignment.Top)
        TabContent() { MySample2() }.tabBar('播放本地媒体').align(Alignment.Top)
        TabContent() { MySample3() }.tabBar('码流').align(Alignment.Top)
        TabContent() { MySample4() }.tabBar('轨道').align(Alignment.Top)
        TabContent() { MySample5() }.tabBar('外挂字幕').align(Alignment.Top)
        TabContent() { MySample6() }.tabBar('通过 dataSrc 的方式播放').align(Alignment.Top)
      }
      .scrollable(true)
      .barMode(BarMode.Scrollable)
      .layoutWeight(1)
    }
  }
}

@Component
struct MySample1 {

  @State message: string = ""

  /*
   * XComponentController - 用于和绑定的 XComponent 之间的交互
   * AVPlayer - 播放器(可以播放视频或音频)
   *   media.createAVPlayer() - 创建一个 AVPlayer 对象
   *   url/fdSrc - 需要播放的视频或音频的 url 地址或 fd 地址(支持 mp4, mp3, hls, dash 等)
   *     注:仅 AVPlayer 处于 idle 状态时,才能设置 url/fdSrc 属性,设置 url/fdSrc 后,播放器会变为 initialized 状态
   *   setPlaybackStrategy() - 设置播放参数(一个 PlaybackStrategy 对象)
   *     preferredBufferDuration - 缓冲持续时间(单位:秒)
   *     preferredBufferDurationForPlaying - 起播缓冲的缓冲持续时间(单位:秒),要求 api 18 或以上
   *     showFirstFrameOnPrepare - 播放器 prepared 后是否显示起播首帧,默认不显示,要求 api 17 或以上
   *   on('startRenderFrame')/off('startRenderFrame') - 首帧渲染时的回调
   *   on('error')/off('error') - 发生错误时的回调
   *     建议在此回调内,调用 reset() 重置播放器
   *   on('durationUpdate')/off('durationUpdate')- 媒体源的总时长发生变化时的回调
   *   on('timeUpdate')/off('timeUpdate') - 播放进度发生变化时的回调
   *   on('videoSizeChange')/off('videoSizeChange') - 视频的分辨率发生变化时的回调
   *   on('bufferingUpdate')/off('bufferingUpdate') - 缓冲相关的回调
   *     infoType - 当前回调的值的类型(BufferingInfoType 枚举)
   *       BUFFERING_START - 缓冲开始
   *       BUFFERING_END - 缓冲结束
   *       BUFFERING_PERCENT - 缓冲百分比,即缓冲进度,到 100% 了则缓冲结束
   *       CACHED_DURATION - 已缓冲数据的可播时长
   *     value - 值
   *   on('speedDone')/off('speedDone') - 调用 setSpeed() 成功之后的回调
   *   on('volumeChange')/off('volumeChange') - 调用 setVolume() 成功之后的回调
   *   on('seekDone')/off('seekDone') - 调用 seek() 之后,视频开始播放时的回调
   *   on('stateChange')/off('stateChange') - 播放状态发生变化时的回调
   *     idle, initialized, prepared, playing, paused, completed, stopped, released, error
   *     调用 reset() 后,播放器会变为 idle 状态
   *     设置 url/fdSrc 后,播放器会变为 initialized 状态
   *     在进入 initialized 状态时,设置播放器的 surfaceId 属性,以及调用 prepare() 方法
   *     在进入 prepared 状态后,就可以调用播放器的 play() 方法了
   *   play() - 播放
   *   pause() - 暂停
   *   stop() - 停止
   *   reset() - 重置,播放器会变为 idle 状态
   *   release() - 清理资源
   *   seek() - seek 操作
   *     timeMs - 需要跳转到的时间点
   *     mode - 基于视频 i 帧的跳转模式(SeekMode 枚举)
   *       SEEK_NEXT_SYNC - 跳转到指定时间点的下一个关键帧
   *       SEEK_PREV_SYNC - 跳转到指定时间点的上一个关键帧
   *       SEEK_CLOSEST - 跳转到距离指定时间点最近的关键帧
   *     注:我这里测试发现,播放 hls 调用 seek 时,指定的 mode 是无效的,每次 seek 后都会从指定的时间点的下一片开始播放(比如 10 秒一片,如果 seek 到 1000 毫秒,则从第 3 片开始播放)
   *   setSpeed() - 设置播放倍速(PlaybackSpeed 枚举)
   *     0.75, 1, 1.25, 1.75, 2, 0.5, 1.5, 3, 0.25, 0.125
   *   setVolume() - 设置播放器的音量(0 -1 之间)
   *   getPlaybackInfo() - 获取播放过程中的相关信息
   *   getPlaybackPosition() - 获取当前的播放位置(要求 api 18 或以上)
   */
  private xComponentController: XComponentController = new XComponentController()
  private avPlayer?: media.AVPlayer

  async createPlayer(surfaceId: string) {
    let avPlayer: media.AVPlayer = await media.createAVPlayer()
    avPlayer.url = 'http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8'
    avPlayer.setPlaybackStrategy({
      preferredBufferDuration: 2
    })
    this.avPlayer = avPlayer

    avPlayer.on('startRenderFrame', () => {
      this.message += 'startRenderFrame\n'
    });
    avPlayer.on('error', (err: BusinessError) => {
      this.message += `error: ${err.code}, ${err.message}\n`
      // 重置播放器,之后播放器会进入 idle 状态
      avPlayer.reset()
    });
    avPlayer.on('durationUpdate', (duration: number) => {
      this.message += `durationUpdate: ${duration}\n`
    });
    avPlayer.on('timeUpdate', (time: number) => {
      // this.message += `timeUpdate: ${time}\n`
    });
    avPlayer.on('videoSizeChange', (width: number, height: number) => {
      this.message += `videoSizeChange: ${width}, ${height}\n`
    });
    avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
      // this.message += `bufferingUpdate: ${infoType}, ${value}\n`
    });
    avPlayer.on('speedDone', (speed: number) => {
      this.message += `speedDone: ${speed}\n`
    });
    avPlayer.on('volumeChange', (volume: number) => {
      this.message += `volumeChange: ${volume}\n`
    });
    avPlayer.on('seekDone', (seekDoneTime: number) => {
      this.message += `seekDone: ${seekDoneTime}\n`
    });
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.message += `stateChange: ${state}, ${reason}\n`
      switch (state) {
        case 'idle':
          break;
        case 'initialized':
          // 在进入 initialized 状态时,设置播放器的 surfaceId 属性,以及调用 prepare() 方法
          avPlayer.surfaceId = surfaceId
          avPlayer.prepare()
          break;
        case 'prepared':
          // 播放
          avPlayer.play()
          break;
        case 'playing':
          break;
        case 'paused':
          break;
        case 'completed':
          break;
        case 'stopped':
          break;
        case 'released':
          break;
        case 'error':
          break;
        default:
          break;
      }
    });
  }

  // 释放资源
  release(): void {
    this.avPlayer?.off('startRenderFrame')
    this.avPlayer?.off('error')
    this.avPlayer?.off('durationUpdate')
    this.avPlayer?.off('timeUpdate')
    this.avPlayer?.off('videoSizeChange')
    this.avPlayer?.off('bufferingUpdate')
    this.avPlayer?.off('speedDone')
    this.avPlayer?.off('volumeChange')
    this.avPlayer?.off('seekDone')
    this.avPlayer?.off('stateChange')
    this.avPlayer?.stop()
    this.avPlayer?.reset()
    this.avPlayer?.release()
  }

  build() {
    Column({ space: 10 }) {

      /*
       * XComponent - 用于绘制媒体内容
       *   id - 标识
       *   type - 写死 XComponentType.SURFACE 即可
       *   controller - 绑定的 XComponentController 对象
       *   onLoad() - 加载完成时的回调
       * XComponentController - 用于和绑定的 XComponent 之间的交互
       *   getXComponentSurfaceId() - 获取 surfaceId(注:在 XComponent 的 onLoad 回调之后才能获取到 surfaceId)
       */
      XComponent({
        id: 'xComponentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .onLoad(async () => {
          // 获取 surfaceId
          // 需要在 AVPlayer 的 initialized 状态时,赋值给 AVPlayer 的 surfaceId 属性
          const surfaceId = this.xComponentController.getXComponentSurfaceId()
          this.createPlayer(surfaceId)
        })
        .width('100%')
        .height(200)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceAround, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
        Button('play').onClick(() => {
          this.avPlayer?.play()
        })
        Button('pause').onClick(() => {
          this.avPlayer?.pause()
        })
        Button('stop').onClick(() => {
          this.avPlayer?.stop()
        })
        Button('reset').onClick(() => {
          this.avPlayer?.reset()
        })
        Button('release').onClick(() => {
          this.avPlayer?.release()
        })
        Button('seek').onClick(() => {
          this.avPlayer?.seek(5_000, media.SeekMode.SEEK_CLOSEST)
        })
        Button('setSpeed').onClick(() => {
          this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_3_00_X)
        })
        Button('setVolume').onClick(() => {
          this.avPlayer?.setVolume(0.5)
        })
        Button('getPlaybackInfo').onClick(async () => {
          let playbackInfo = await this.avPlayer?.getPlaybackInfo()
          this.message += `${JSON.stringify(playbackInfo)}`
        })
      }

      Scroll() {
        Text(this.message)
      }
      .layoutWeight(1).align(Alignment.TopStart).backgroundColor(Color.Yellow).width('100%')
    }
  }
}

@Component
struct MySample2 {

  @State message: string = ""

  /*
   * AVPlayer - 播放器(可以播放视频或音频)
   *   url/fdSrc - 需要播放的视频或音频的 url 地址或 fd 地址(支持 mp4, mp3, hls, dash 等)
   *     注:仅 AVPlayer 处于 idle 状态时,才能设置 url/fdSrc 属性,设置 url/fdSrc 后,播放器会变为 initialized 状态
   */
  private xComponentController: XComponentController = new XComponentController()
  private avPlayer?: media.AVPlayer

  async createPlayer(surfaceId: string) {
    let avPlayer: media.AVPlayer = await media.createAVPlayer();
    this.avPlayer = avPlayer

    avPlayer.on('error', (err: BusinessError) => {
      this.message += `error: ${err.code}, ${err.message}\n`
      avPlayer.reset()
    });
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.message += `stateChange: ${state}, ${reason}\n`
      switch (state) {
        case 'initialized':
          avPlayer.surfaceId = surfaceId
          avPlayer.prepare()
          break;
        case 'prepared':
          avPlayer.play()
          break;
        case 'completed':
          // 播放完成后,调用 reset() 将播放器置为 idle 状态
          avPlayer.reset()
          break;
        default:
          break;
      }
    });
  }

  aboutToAppear(): void {
    // 复制 rawfile 中的指定的文件到指定的沙箱地址
    let filePath = getContext(this).filesDir + "/1.mp3"
    getContext(this).resourceManager.getRawFileContent('audio/1.mp3', (err, value) => {
      let myBuffer: ArrayBufferLike = value.buffer
      let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
      fs.writeSync(file.fd, myBuffer)
      fs.closeSync(file)
    })
  }

  build() {
    Column({ space: 10 }) {

      XComponent({
        id: 'xComponentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      }).onLoad(async () => {
        this.createPlayer(this.xComponentController.getXComponentSurfaceId())
      }).width('100%').height(200)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceAround, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
        Button('播放本地的资源媒体').onClick(async () => {
          // 将本地的资源媒体转为 AVFileDescriptor 并赋值给 AVPlayer 的 fdSrc 属性
          let fileDescriptor = await getContext(this).resourceManager.getRawFd('audio/1.mp3')
          // let avFileDescriptor: media.AVFileDescriptor = { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length }
          if (this.avPlayer) {
            this.avPlayer.fdSrc = fileDescriptor
          }
        })

        Button('播放本地的沙箱媒体').onClick(async () => {
          // 将本地的沙箱媒体转为 fd:// 地址并赋值给 AVPlayer 的 url 属性
          let filePath = getContext(this).filesDir + "/1.mp3"
          let file = fs.openSync(filePath);
          let fdPath = 'fd://' + file.fd;
          if (this.avPlayer) {
            this.avPlayer.url = fdPath;
          }
          fs.closeSync(file)
        })
      }

      Scroll() {
        Text(this.message)
      }
      .layoutWeight(1).align(Alignment.TopStart).backgroundColor(Color.Yellow).width('100%')
    }
  }
}

@Component
struct MySample3 {

  bitrates: number[] = []
  currentBitrateIndex: number = -1
  @State message: string = ""

  /*
   * AVPlayer - 播放器(可以播放视频或音频)
   *   on('availableBitrates')/off('availableBitrates') - 拿到 hls 或 dash 的可用的码流列表时的回调
   *   on('bitrateDone')/off('bitrateDone') -  调用 setBitrate() 成功之后的回调
   *     注:并不是当前播放的码流发生变化时触发这个回调,而是调用 setBitrate() 成功之后触发这个回调,而调用 setBitrate() 后并不会马上播放对应的码流,而只是在下载新片时下载指定的码流切片
   *   setBitrate() - 指定播放的码流
   *     具体的码流可以从 on('availableBitrates') 获取到
   *     默认是码流自适应的,调用 setBitrate() 后就会尝试播放指定的码流,但是不知道怎么再变回码流自适应
   *     调用 setBitrate() 后并不会马上播放对应的码流,而只是在下载新片时下载指定的码流切片
   */
  private xComponentController: XComponentController = new XComponentController()
  private avPlayer?: media.AVPlayer

  async createPlayer(surfaceId: string) {
    let avPlayer: media.AVPlayer = await media.createAVPlayer();
    avPlayer.url = 'http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8'
    this.avPlayer = avPlayer

    avPlayer.on('error', (err: BusinessError) => {
      this.message += `error: ${err.code}, ${err.message}\n`
      avPlayer.reset()
    });
    avPlayer.on('availableBitrates', (bitrates: number[]) => {
      this.bitrates = bitrates
      this.message += `availableBitrates: ${JSON.stringify(bitrates)}\n`
    });
    avPlayer.on('bitrateDone', (bitrate: number) => {
      this.message += `bitrateDone: ${bitrate}\n`
    });
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.message += `stateChange: ${state}, ${reason}\n`
      switch (state) {
        case 'initialized':
          avPlayer.surfaceId = surfaceId
          avPlayer.prepare()
          break;
        case 'prepared':
          avPlayer.play()
          break;
        default:
          break;
      }
    });
  }

  build() {
    Column({ space: 10 }) {

      XComponent({
        id: 'xComponentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      }).onLoad(async () => {
        this.createPlayer(this.xComponentController.getXComponentSurfaceId())
      }).width('100%').height(200)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceAround, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
        Button('switchBitrate').onClick(() => {
          if (this.bitrates.length > 0) {
            this.currentBitrateIndex += 1
            if (this.currentBitrateIndex > this.bitrates.length - 1) {
              this.currentBitrateIndex = 0
            }
          }
          this.avPlayer?.setBitrate(this.bitrates[this.currentBitrateIndex])
        })

        Scroll() {
          Text(this.message)
        }
        .layoutWeight(1).align(Alignment.TopStart).backgroundColor(Color.Yellow).width('100%')
      }
    }
  }
}

@Component
struct MySample4 {

  @State message: string = ""

  /*
   * AVPlayer - 播放器(可以播放视频或音频)
   *   getTrackDescription() - 获取每条轨道的信息(返回一个 MediaDescription 数组)
   *     轨道的类型有:音频,视频,字幕
   *     可以通过 key/value 的方式获,从 MediaDescription 中获取轨道的指定的信息
   *     MediaDescriptionKey.MD_KEY_TRACK_INDEX - 轨道的索引
   *     MediaDescriptionKey.MD_KEY_TRACK_TYPE - 轨道的类型(0是音频,1是视频,2是字幕)
   *     MediaDescriptionKey.MD_KEY_TRACK_NAME - 轨道的名称
   *     MediaDescriptionKey.MD_KEY_CODEC_MIME - 音频或视频轨道的编码类型
   *     MediaDescriptionKey.MD_KEY_DURATION - 音频或视频轨道的时长(单位:ms)
   *     MediaDescriptionKey.MD_KEY_BITRATE - 音频或视频轨道的码流(单位:bps)
   *     MediaDescriptionKey.MD_KEY_WIDTH - 视频轨道的宽(单位:px)
   *     MediaDescriptionKey.MD_KEY_HEIGHT - 视频轨道的高(单位:px)
   *     MediaDescriptionKey.MD_KEY_FRAME_RATE - 视频轨道的帧率
   *     MediaDescriptionKey.MD_KEY_AUD_CHANNEL_COUNT - 音频轨道的声道数
   *     MediaDescriptionKey.MD_KEY_AUD_SAMPLE_RATE - 音频轨道的采样率
   *     MediaDescriptionKey.MD_KEY_AUD_SAMPLE_DEPTH - 音频轨道的位深
   *   getSelectedTracks() - 获取当前播放的轨道的索引的集合
   *   selectTrack() - 播放指定索引的轨道
   *   deselectTrack() - 取消指定索引的轨道的播放
   *   on(‘trackChange’)/off(‘trackChange’) - 播放的轨道发生变化时的回调
   *   on(‘trackInfoUpdate’)/off(‘trackInfoUpdate’) - 播放的轨道的信息更新时的回调
   */
  private xComponentController: XComponentController = new XComponentController()
  private avPlayer?: media.AVPlayer

  async createPlayer(surfaceId: string) {
    let avPlayer: media.AVPlayer = await media.createAVPlayer();
    avPlayer.url = 'http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8'
    this.avPlayer = avPlayer

    avPlayer.on('error', (err: BusinessError) => {
      this.message += `error: ${err.code}, ${err.message}\n`
      avPlayer.reset()
    });
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.message += `stateChange: ${state}, ${reason}\n`
      switch (state) {
        case 'initialized':
          avPlayer.surfaceId = surfaceId
          avPlayer.prepare()
          break;
        case 'prepared':
          avPlayer.play()
          break;
        default:
          break;
      }
    });
  }

  build() {
    Column({ space: 10 }) {

      XComponent({
        id: 'xComponentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      }).onLoad(async () => {
        this.createPlayer(this.xComponentController.getXComponentSurfaceId())
      }).width('100%').height(200)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceAround, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
        Button('获取轨道信息').onClick(async () => {
          let trackList = await this.avPlayer?.getTrackDescription()
          if (trackList != null && trackList.length > 0) {
            for (let i = 0; i < trackList.length; i++) {
              let mediaDescription = trackList[i]
              this.message += `index:${mediaDescription[media.MediaDescriptionKey.MD_KEY_TRACK_INDEX]}\n`
              this.message += `type:${mediaDescription[media.MediaDescriptionKey.MD_KEY_TRACK_TYPE]}\n`
              this.message += `name:${mediaDescription[media.MediaDescriptionKey.MD_KEY_TRACK_NAME]}\n`
              this.message += `mime:${mediaDescription[media.MediaDescriptionKey.MD_KEY_CODEC_MIME]}\n`
            }
          }
        })
      }

      Scroll() {
        Text(this.message)
      }
      .layoutWeight(1).align(Alignment.TopStart).backgroundColor(Color.Yellow).width('100%')
    }
  }
}

@Component
struct MySample5 {

  @State message: string = ""

  /*
   * AVPlayer - 播放器(可以播放视频或音频)
   *   addSubtitleFromFd() - 挂载 fd 地址的字幕
   *   addSubtitleFromUrl() - 挂载 url 地址的字幕
   *   on('subtitleUpdate')/off('subtitleUpdate') - 当前显示的字幕更新时的回调(仅外挂字幕有效),回调参数是一个 SubtitleInfo 对象
   *     text - 字幕文本
   *     startTime - 当前显示的字幕的开始时间(单位:毫秒)
   *     duration - 当前显示的字幕的持续时间(单位:毫秒)
   *
   * 注:
   * 1、本例演示的是如何拿到当前应该显示的字幕文本,至于文本如何显示则需要自己写代码
   * 2、如果是内置字幕的话,即视频的字幕轨道,请参见 MySample4 中的相关说明
   */
  private xComponentController: XComponentController = new XComponentController()
  private avPlayer?: media.AVPlayer

  async createPlayer(surfaceId: string) {
    let avPlayer: media.AVPlayer = await media.createAVPlayer();
    avPlayer.url = 'http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8'
    this.avPlayer = avPlayer

    // 获取本地字幕文件的 FileDescriptor
    let fileDescriptor = await getContext(this).resourceManager.getRawFd('mysrt.srt')
    // 挂载 fd 地址的字幕
    avPlayer.addSubtitleFromFd(fileDescriptor.fd, fileDescriptor.offset, fileDescriptor.length);
    // 挂载 url 地址的字幕
    // avPlayer.addSubtitleFromUrl('http://a.b.c/mysrt.srt')

    avPlayer.on('error', (err: BusinessError) => {
      this.message += `error: ${err.code}, ${err.message}\n`
      avPlayer.reset()
    });
    avPlayer.on('subtitleUpdate', (info: media.SubtitleInfo) => {
      // 当前显示的字幕更新时
      this.message += `text:${info.text}, startTime:${info.startTime}, duration:${info.duration}\n`
    });
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.message += `stateChange: ${state}, ${reason}\n`
      switch (state) {
        case 'initialized':
          avPlayer.surfaceId = surfaceId
          avPlayer.prepare()
          break;
        case 'prepared':
          avPlayer.play()
          break;
        default:
          break;
      }
    });
  }

  build() {
    Column({ space: 10 }) {

      XComponent({
        id: 'xComponentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      }).onLoad(async () => {
        this.createPlayer(this.xComponentController.getXComponentSurfaceId())
      }).width('100%').height(200)

      Scroll() {
        Text(this.message)
      }
      .layoutWeight(1).align(Alignment.TopStart).backgroundColor(Color.Yellow).width('100%')
    }
  }
}

@Component
struct MySample6 {

  @State message: string = ""

  /*
   * AVPlayer - 播放器(可以播放视频或音频)
   *   dataSrc - 需要播放的视频流或音频流
   *     注:仅 AVPlayer 处于 idle 状态时,才能设置 dataSrc 属性,设置 dataSrc 后,播放器会变为 initialized 状态
   */
  private xComponentController: XComponentController = new XComponentController()
  private avPlayer?: media.AVPlayer

  async createPlayer(surfaceId: string) {
    let avPlayer: media.AVPlayer = await media.createAVPlayer();
    this.avPlayer = avPlayer

    avPlayer.on('error', (err: BusinessError) => {
      this.message += `error: ${err.code}, ${err.message}\n`
      avPlayer.reset()
    });
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.message += `stateChange: ${state}, ${reason}\n`
      switch (state) {
        case 'initialized':
          avPlayer.surfaceId = surfaceId
          avPlayer.prepare()
          break;
        case 'prepared':
          avPlayer.play()
          break;
        case 'completed':
          // 播放完成后,调用 reset() 将播放器置为 idle 状态
          avPlayer.reset()
          break;
        default:
          break;
      }
    });
  }

  build() {
    Column({ space: 10 }) {

      XComponent({
        id: 'xComponentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      }).onLoad(async () => {
        this.createPlayer(this.xComponentController.getXComponentSurfaceId())
      }).width('100%').height(200)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceAround, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
        Button('通过 dataSrc 的方式播放').onClick(async () => {
          let myBuffer: ArrayBuffer = (await getContext(this).resourceManager.getRawFileContent('video/mp4.mp4')).buffer as ArrayBuffer
          /*
           * AVDataSrcDescriptor - 数据流描述符
           *   fileSize - 流的总大小,如果是 -1 则代表不确定(比如直播)
           *   callback - 需要在此回调中填充数据,此回调可能会被多次回调
           *     buffer - 当前需要填充的缓冲区
           *     len - 当前需要填充的缓冲区的长度
           *     pos - 当前需要填充的数据在源文件中的位置
           *     返回值为当前成功填充的数据的长度,返回 -1 代表到末尾了,返回 -2 代表遇到异常了
           */
          let dataSrcDescriptor: media.AVDataSrcDescriptor = {
            fileSize: myBuffer.byteLength,
            callback: (buffer, len, pos) => {
              MyLog.d(`callback: buffer:${buffer.byteLength}, len,${len}, pos:${pos}`)
              if (buffer == undefined || len == undefined || pos == undefined) {
                this.message += "dataSrc callback param invalid"
                return -1;
              }
              if (pos >= myBuffer.byteLength) {
                return -1
              }

              const targetArray = new Uint8Array(buffer);
              if (pos + len >= myBuffer.byteLength) {
                const sourceArray = new Uint8Array(myBuffer.slice(pos, myBuffer.byteLength));
                targetArray.set(sourceArray);
                return myBuffer.byteLength - pos
              } else {
                const sourceArray = new Uint8Array(myBuffer.slice(pos, pos + len));
                targetArray.set(sourceArray);
                return len
              }
            }
          };

          if (this.avPlayer) {
            this.avPlayer.dataSrc = dataSrcDescriptor
          }
        })
      }

      Scroll() {
        Text(this.message)
      }
      .layoutWeight(1).align(Alignment.TopStart).backgroundColor(Color.Yellow).width('100%')
    }
  }
}

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

posted @ 2025-05-27 14:45  webabcd  阅读(244)  评论(0)    收藏  举报