HarmonyOS开发中的视频播放:从AVPlayer到播放器封装的全链路实战

举报
Jack20 发表于 2026/06/20 21:10:02 2026/06/20
【摘要】 HarmonyOS开发中的视频播放:从AVPlayer到播放器封装的全链路实战核心要点:掌握HarmonyOS视频播放的核心API——AVPlayer与Video组件,理解播放状态机的流转逻辑,学会封装一个生产级可复用的视频播放器。 一、背景与动机你有没有这样的经历——周末窝在沙发上刷短视频,手指一划,视频"嗖"地就开始播放了,丝滑得像抹了黄油。但当你自己动手写一个视频播放器时,才发现事情...

HarmonyOS开发中的视频播放:从AVPlayer到播放器封装的全链路实战

核心要点:掌握HarmonyOS视频播放的核心API——AVPlayer与Video组件,理解播放状态机的流转逻辑,学会封装一个生产级可复用的视频播放器。


一、背景与动机

你有没有这样的经历——周末窝在沙发上刷短视频,手指一划,视频"嗖"地就开始播放了,丝滑得像抹了黄油。但当你自己动手写一个视频播放器时,才发现事情远没那么简单:视频加载不出来、播放到一半突然黑屏、切后台回来声音还在播画面却卡住了……

视频播放,看似简单,实则暗藏玄机。它涉及媒体文件的解封装、解码、渲染、音频输出等多个环节,任何一个环节出问题,用户体验就会"翻车"。

HarmonyOS为我们提供了两套视频播放方案:底层AVPlayer高层Video组件。前者灵活强大,适合做定制化播放器;后者开箱即用,适合快速集成。但无论选哪个,理解播放状态机都是绕不过去的坎儿。

今天这篇文章,我们就从零开始,把视频播放这件事彻底搞明白。


二、核心原理

2.1 视频播放的技术链路

一段视频从文件到屏幕,要经历这样的旅程:

flowchart LR
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    A[视频文件/]:::primary --> B[解封装 Demux]:::warning
    B --> C[视频解码 Decode]:::error
    B --> D[音频解码 Decode]:::error
    C --> E[视频渲染 Surface]:::info
    D --> F[音频输出 AudioRenderer]:::purple
    E --> G[屏幕显示]:::primary
    F --> H[扬声器/耳机]:::primary

简单来说:解封装把MP4/MKV等容器格式拆成视频流和音频流,解码把压缩的数据还原成原始画面和声音,渲染和输出则把画面送到屏幕、声音送到扬声器。

2.2 AVPlayer播放状态机

AVPlayer是HarmonyOS提供的底层播放器,它的核心是状态机。理解状态机,就理解了播放器的"灵魂"。

stateDiagram-v2
    classDef primaryStyle fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warningStyle fill:#FFB74D,stroke:#F57C00,color:#000
    classDef errorStyle fill:#EF5350,stroke:#C62828,color:#fff
    classDef infoStyle fill:#81C784,stroke:#388E3C,color:#000
    classDef purpleStyle fill:#CE93D8,stroke:#7B1FA2,color:#000

    [*] --> idle : 创建AVPlayer
    idle --> initialized : setSource()
    initialized --> prepared : prepare()
    prepared --> playing : play()
    prepared --> paused : pause()
    playing --> paused : pause()
    paused --> playing : play()
    playing --> completed : 自然播完
    completed --> playing : play() 重播
    completed --> stopped : stop()
    prepared --> stopped : stop()
    playing --> stopped : stop()
    paused --> stopped : stop()
    stopped --> prepared : prepare() 重新准备
    stopped --> idle : reset()
    initialized --> idle : reset()
    any --> error : 出错
    error --> idle : reset()

关键状态说明

状态 含义 可执行操作
idle 空闲,刚创建 setSource()
initialized 已设置数据源 prepare()
prepared 准备就绪 play()、pause()、stop()、seek()
playing 播放中 pause()、stop()、seek()
paused 暂停 play()、stop()、seek()
completed 播放完成 play()(重播)、stop()、seek()
stopped 停止 prepare()、reset()
error 错误 reset()

⚠️ 核心规则:状态机是严格的,不能跳步!你必须从idle → initialized → prepared → playing,不能直接从idle跳到playing。违反状态转换规则会导致错误或无效操作。

2.3 Video组件 vs AVPlayer

特性 Video组件 AVPlayer
抽象层级 高层UI组件 底层媒体引擎
使用难度 简单,声明式 较复杂,需管理状态机
自定义能力 有限 完全可控
Surface管理 自动 需手动绑定XComponent
适用场景 快速集成、简单播放 自定义播放器、直播、画中画
性能控制 较弱 精细可控

三、代码实战

3.1 基础实战:使用Video组件快速播放视频

Video组件是最简单的视频播放方案,适合快速集成场景:

// VideoComponentDemo.ets
// 使用Video组件实现基础视频播放

@Entry
@Component
struct VideoComponentDemo {
  // 视频控制器,用于控制播放、暂停、进度跳转
  private videoController: VideoController = new VideoController()
  
  // 播放状态
  @State isPlaying: boolean = false
  @State currentTime: number = 0
  @State duration: number = 0

  build() {
    Column() {
      // 视频播放区域
      Video({
        src: $rawfile('sample.mp4'), // rawfile目录下的视频文件
        controller: this.videoController
      })
        .width('100%')
        .height(280)
        .autoPlay(false) // 不自动播放,等用户点击
        .muted(false) // 不静音
        .loop(false) // 不循环播放
        .controls(true) // 显示默认控制栏
        .objectFit(ImageFit.Contain) // 视频缩放模式
        .onPrepared(() => {
          console.info('[Video] 准备完成,可以播放了')
        })
        .onPlaying(() => {
          this.isPlaying = true
          console.info('[Video] 开始播放')
        })
        .onPause(() => {
          this.isPlaying = false
          console.info('[Video] 暂停播放')
        })
        .onFinish(() => {
          this.isPlaying = false
          console.info('[Video] 播放完成')
        })
        .onError(() => {
          console.error('[Video] 播放出错')
        })
        .onTimeUpdate((time: number) => {
          this.currentTime = time // 更新当前播放进度
        })
        .onDurationChange((duration: number) => {
          this.duration = duration // 获取视频总时长
        })

      // 自定义控制按钮
      Row() {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => {
            if (this.isPlaying) {
              this.videoController.pause()
            } else {
              this.videoController.start()
            }
          })

        Button('快进10s')
          .onClick(() => {
            // 跳转到当前进度+10秒的位置
            this.videoController.setCurrentTime(this.currentTime + 10000)
          })

        Button('从头播放')
          .onClick(() => {
            this.videoController.setCurrentTime(0)
            this.videoController.start()
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(16)

      // 进度显示
      Text(`播放进度: ${this.formatTime(this.currentTime)} / ${this.formatTime(this.duration)}`)
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }

  // 格式化时间(毫秒 → mm:ss)
  private formatTime(ms: number): string {
    const totalSeconds = Math.floor(ms / 1000)
    const minutes = Math.floor(totalSeconds / 60)
    const seconds = totalSeconds % 60
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }
}

3.2 进阶实战:使用AVPlayer构建可控播放器

AVPlayer提供了更底层的控制能力,适合需要深度定制的场景:

// AVPlayerDemo.ets
// 使用AVPlayer实现完全可控的视频播放器

import { media } from '@kit.MediaKit'
import { common } from '@kit.AbilityKit'

@Entry
@Component
struct AVPlayerDemo {
  // AVPlayer实例
  private avPlayer: media.AVPlayer | null = null
  // XComponent控制器,用于获取Surface
  private xComponentController: XComponentController = new XComponentController()
  
  // 播放状态
  @State playerState: string = 'idle'
  @State isPlaying: boolean = false
  @State currentTime: number = 0
  @State duration: number = 0
  @State volume: number = 1.0
  @State speed: number = 1.0
  @State errorMessage: string = ''

  // 播放速度选项
  private speedOptions: number[] = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]

  aboutToAppear() {
    this.initAVPlayer()
  }

  aboutToDisappear() {
    this.releasePlayer()
  }

  // 初始化AVPlayer
  private async initAVPlayer() {
    try {
      // 第一步:创建AVPlayer实例
      this.avPlayer = await media.createAVPlayer()
      
      // 第二步:监听状态变化(核心!)
      this.avPlayer.on('stateChange', (state: string) => {
        this.playerState = state
        console.info(`[AVPlayer] 状态变化: ${state}`)
        
        switch (state) {
          case 'initialized':
            // initialized状态下需要设置Surface,然后prepare
            this.setSurfaceAndPrepare()
            break
          case 'prepared':
            // 准备完成,可以获取duration等信息
            console.info('[AVPlayer] 准备完成')
            break
          case 'completed':
            this.isPlaying = false
            console.info('[AVPlayer] 播放完成')
            break
          case 'stopped':
            this.isPlaying = false
            console.info('[AVPlayer] 已停止')
            break
        }
      })

      // 第三步:监听其他事件
      this.avPlayer.on('error', (err) => {
        this.errorMessage = `播放错误: ${err.message}`
        console.error(`[AVPlayer] 错误: ${JSON.stringify(err)}`)
      })

      this.avPlayer.on('timeUpdate', (time: number) => {
        this.currentTime = time
      })

      this.avPlayer.on('durationUpdate', (duration: number) => {
        this.duration = duration
      })

      // 第四步:设置数据源
      const context = getContext(this) as common.UIAbilityContext
      const fdPath = `fd://1` // 实际项目中需要通过fs.open获取fd
      // 也可以用rawfile路径
      const rawfilePath = context.resourceDir + '/rawfile/sample.mp4'
      this.avPlayer.url = rawfilePath

    } catch (err) {
      this.errorMessage = `初始化失败: ${err}`
      console.error(`[AVPlayer] 初始化失败: ${err}`)
    }
  }

  // 设置Surface并准备播放
  private async setSurfaceAndPrepare() {
    if (!this.avPlayer) return
    try {
      // 从XComponent获取Surface ID
      const surfaceId = this.xComponentController.getXComponentSurfaceId()
      this.avPlayer.surfaceId = surfaceId
      
      // 调用prepare进入prepared状态
      await this.avPlayer.prepare()
    } catch (err) {
      console.error(`[AVPlayer] 准备失败: ${err}`)
    }
  }

  // 释放播放器资源
  private async releasePlayer() {
    if (this.avPlayer) {
      try {
        await this.avPlayer.release()
        this.avPlayer = null
        console.info('[AVPlayer] 资源已释放')
      } catch (err) {
        console.error(`[AVPlayer] 释放失败: ${err}`)
      }
    }
  }

  build() {
    Column() {
      // 视频渲染区域 - 使用XComponent提供Surface
      XComponent({
        id: 'videoSurface',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .width('100%')
        .height(280)
        .onLoad(() => {
          console.info('[XComponent] Surface加载完成')
        })

      // 状态显示
      Text(`当前状态: ${this.playerState}`)
        .fontSize(12)
        .fontColor('#aaaaaa')
        .margin({ top: 8 })

      // 错误提示
      if (this.errorMessage) {
        Text(this.errorMessage)
          .fontSize(12)
          .fontColor('#EF5350')
          .margin({ top: 4 })
      }

      // 播放控制按钮
      Row() {
        Button('播放')
          .enabled(this.playerState === 'prepared' || this.playerState === 'paused' || this.playerState === 'completed')
          .onClick(() => {
            this.avPlayer?.play()
            this.isPlaying = true
          })

        Button('暂停')
          .enabled(this.playerState === 'playing')
          .onClick(() => {
            this.avPlayer?.pause()
            this.isPlaying = false
          })

        Button('停止')
          .enabled(this.playerState === 'playing' || this.playerState === 'paused')
          .onClick(() => {
            this.avPlayer?.stop()
          })

        Button('重置')
          .enabled(this.playerState === 'stopped' || this.playerState === 'error')
          .onClick(() => {
            this.avPlayer?.reset()
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(16)

      // 进度条
      Row() {
        Text(this.formatTime(this.currentTime))
          .fontSize(12)
          .fontColor('#ffffff')
        
        Slider({
          value: this.currentTime,
          min: 0,
          max: this.duration > 0 ? this.duration : 1,
          step: 1000
        })
          .width('60%')
          .onChange((value: number) => {
            if (this.avPlayer && this.playerState === 'playing') {
              this.avPlayer.seek(value)
            }
          })
        
        Text(this.formatTime(this.duration))
          .fontSize(12)
          .fontColor('#ffffff')
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ left: 16, right: 16 })

      // 音量控制
      Row() {
        Text('音量:')
          .fontSize(14)
          .fontColor('#ffffff')
        
        Slider({
          value: this.volume * 100,
          min: 0,
          max: 100
        })
          .width('50%')
          .onChange((value: number) => {
            this.volume = value / 100
            if (this.avPlayer) {
              this.avPlayer.setVolume(this.volume)
            }
          })
        
        Text(`${Math.round(this.volume * 100)}%`)
          .fontSize(14)
          .fontColor('#ffffff')
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding(16)

      // 播放速度选择
      Row() {
        Text('倍速:')
          .fontSize(14)
          .fontColor('#ffffff')
        
        ForEach(this.speedOptions, (speed: number) => {
          Button(`${speed}x`)
            .fontSize(12)
            .height(32)
            .backgroundColor(this.speed === speed ? '#4FC3F7' : '#333333')
            .onClick(() => {
              this.speed = speed
              if (this.avPlayer) {
                this.avPlayer.setSpeed(speed as media.PlaybackSpeed)
              }
            })
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }

  // 格式化时间
  private formatTime(ms: number): string {
    const totalSeconds = Math.floor(ms / 1000)
    const minutes = Math.floor(totalSeconds / 60)
    const seconds = totalSeconds % 60
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }
}

3.3 高级实战:封装生产级视频播放器组件

在实际项目中,我们不可能每次都从头写播放逻辑。封装一个可复用的播放器组件是必须的:

// VideoPlayerKit.ets
// 生产级视频播放器封装组件

import { media } from '@kit.MediaKit'
import { common } from '@kit.AbilityKit'

// 播放器状态枚举
export enum PlayerState {
  IDLE = 'idle',
  INITIALIZING = 'initializing',
  INITIALIZED = 'initialized',
  PREPARING = 'preparing',
  PREPARED = 'prepared',
  PLAYING = 'playing',
  PAUSED = 'paused',
  COMPLETED = 'completed',
  STOPPED = 'stopped',
  RELEASED = 'released',
  ERROR = 'error'
}

// 播放器配置接口
export interface VideoPlayerConfig {
  autoPlay?: boolean       // 是否自动播放,默认false
  loop?: boolean           // 是否循环播放,默认false
  muted?: boolean          // 是否静音,默认false
  volume?: number          // 音量 0~1,默认1
  speed?: number           // 播放速度,默认1
  startTime?: number       // 起始播放位置(毫秒),默认0
}

// 播放器事件回调接口
export interface VideoPlayerCallbacks {
  onStateChange?: (state: PlayerState) => void
  onTimeUpdate?: (currentTime: number) => void
  onDurationUpdate?: (duration: number) => void
  onError?: (error: string) => void
  onBufferingUpdate?: (percent: number) => void
  onCompletion?: () => void
}

// 封装的视频播放器管理类
export class VideoPlayerManager {
  private avPlayer: media.AVPlayer | null = null
  private state: PlayerState = PlayerState.IDLE
  private config: VideoPlayerConfig = {}
  private callbacks: VideoPlayerCallbacks = {}
  private surfaceId: string = ''
  private _duration: number = 0
  private _currentTime: number = 0

  // 获取当前状态
  get currentState(): PlayerState {
    return this.state
  }

  // 获取总时长
  get duration(): number {
    return this._duration
  }

  // 获取当前播放时间
  get currentTime(): number {
    return this._currentTime
  }

  // 设置事件回调
  setCallbacks(callbacks: VideoPlayerCallbacks) {
    this.callbacks = callbacks
  }

  // 初始化播放器
  async init(surfaceId: string, config?: VideoPlayerConfig): Promise<void> {
    this.surfaceId = surfaceId
    this.config = { autoPlay: false, loop: false, muted: false, volume: 1, speed: 1, ...config }

    try {
      this.updateState(PlayerState.INITIALIZING)
      this.avPlayer = await media.createAVPlayer()
      this.registerListeners()
      this.updateState(PlayerState.IDLE)
    } catch (err) {
      this.updateState(PlayerState.ERROR)
      this.callbacks.onError?.(`初始化失败: ${err}`)
    }
  }

  // 设置数据源并准备
  async setSourceAndPrepare(url: string): Promise<void> {
    if (!this.avPlayer) {
      this.callbacks.onError?.('播放器未初始化')
      return
    }

    try {
      // 设置数据源
      this.avPlayer.url = url
      // 注意:stateChange回调中会处理后续流程
    } catch (err) {
      this.updateState(PlayerState.ERROR)
      this.callbacks.onError?.(`设置数据源失败: ${err}`)
    }
  }

  // 播放
  async play(): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.play()
    } catch (err) {
      this.callbacks.onError?.(`播放失败: ${err}`)
    }
  }

  // 暂停
  async pause(): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.pause()
    } catch (err) {
      this.callbacks.onError?.(`暂停失败: ${err}`)
    }
  }

  // 跳转
  async seek(timeMs: number): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.seek(timeMs, media.SeekMode.SEEK_PREV_SYNC)
    } catch (err) {
      this.callbacks.onError?.(`跳转失败: ${err}`)
    }
  }

  // 停止
  async stop(): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.stop()
    } catch (err) {
      this.callbacks.onError?.(`停止失败: ${err}`)
    }
  }

  // 设置音量
  async setVolume(volume: number): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.setVolume(Math.max(0, Math.min(1, volume)))
    } catch (err) {
      this.callbacks.onError?.(`设置音量失败: ${err}`)
    }
  }

  // 设置播放速度
  async setSpeed(speed: number): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.setSpeed(speed as media.PlaybackSpeed)
    } catch (err) {
      this.callbacks.onError?.(`设置速度失败: ${err}`)
    }
  }

  // 释放资源
  async release(): Promise<void> {
    if (!this.avPlayer) return
    try {
      await this.avPlayer.release()
      this.avPlayer = null
      this.updateState(PlayerState.RELEASED)
    } catch (err) {
      this.callbacks.onError?.(`释放失败: ${err}`)
    }
  }

  // 注册AVPlayer事件监听
  private registerListeners() {
    if (!this.avPlayer) return

    // 状态变化监听(核心)
    this.avPlayer.on('stateChange', async (state: string) => {
      console.info(`[VideoPlayerManager] 状态: ${state}`)

      switch (state) {
        case 'initialized':
          // 设置Surface并准备
          if (this.avPlayer && this.surfaceId) {
            this.avPlayer.surfaceId = this.surfaceId
            await this.avPlayer.prepare()
          }
          break
        case 'prepared':
          this.updateState(PlayerState.PREPARED)
          // 应用配置
          this.applyConfig()
          // 自动播放
          if (this.config.autoPlay) {
            await this.avPlayer?.play()
          }
          break
        case 'playing':
          this.updateState(PlayerState.PLAYING)
          break
        case 'paused':
          this.updateState(PlayerState.PAUSED)
          break
        case 'completed':
          this.updateState(PlayerState.COMPLETED)
          // 循环播放
          if (this.config.loop && this.avPlayer) {
            await this.avPlayer.seek(0)
            await this.avPlayer.play()
          } else {
            this.callbacks.onCompletion?.()
          }
          break
        case 'stopped':
          this.updateState(PlayerState.STOPPED)
          break
        case 'error':
          this.updateState(PlayerState.ERROR)
          break
      }
    })

    // 时间更新
    this.avPlayer.on('timeUpdate', (time: number) => {
      this._currentTime = time
      this.callbacks.onTimeUpdate?.(time)
    })

    // 时长更新
    this.avPlayer.on('durationUpdate', (duration: number) => {
      this._duration = duration
      this.callbacks.onDurationUpdate?.(duration)
    })

    // 错误
    this.avPlayer.on('error', (err) => {
      this.updateState(PlayerState.ERROR)
      this.callbacks.onError?.(err.message || '未知播放错误')
    })

    // 缓冲更新
    this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
      if (infoType === media.BufferingInfoType.BUFFERING_PERCENT) {
        this.callbacks.onBufferingUpdate?.(value)
      }
    })
  }

  // 应用播放配置
  private applyConfig() {
    if (!this.avPlayer) return
    if (this.config.muted !== undefined) {
      this.avPlayer.setVolume(this.config.muted ? 0 : (this.config.volume ?? 1))
    }
    if (this.config.volume !== undefined && !this.config.muted) {
      this.avPlayer.setVolume(this.config.volume)
    }
    if (this.config.speed !== undefined) {
      this.avPlayer.setSpeed(this.config.speed as media.PlaybackSpeed)
    }
    if (this.config.startTime && this.config.startTime > 0) {
      this.avPlayer.seek(this.config.startTime)
    }
  }

  // 更新内部状态并通知外部
  private updateState(state: PlayerState) {
    this.state = state
    this.callbacks.onStateChange?.(state)
  }
}

// ====== 使用封装播放器的UI组件 ======

@Entry
@Component
struct VideoPlayerKitDemo {
  private xComponentController: XComponentController = new XComponentController()
  private playerManager: VideoPlayerManager = new VideoPlayerManager()
  
  @State playerState: PlayerState = PlayerState.IDLE
  @State currentTime: number = 0
  @State duration: number = 0
  @State bufferingPercent: number = 100
  @State showControls: boolean = true

  aboutToAppear() {
    // 配置回调
    this.playerManager.setCallbacks({
      onStateChange: (state: PlayerState) => {
        this.playerState = state
      },
      onTimeUpdate: (time: number) => {
        this.currentTime = time
      },
      onDurationUpdate: (duration: number) => {
        this.duration = duration
      },
      onError: (error: string) => {
        console.error(`[播放器错误] ${error}`)
      },
      onBufferingUpdate: (percent: number) => {
        this.bufferingPercent = percent
      },
      onCompletion: () => {
        console.info('[播放器] 播放完成')
      }
    })
  }

  aboutToDisappear() {
    this.playerManager.release()
  }

  build() {
    Stack() {
      // 视频渲染区域
      XComponent({
        id: 'videoSurface',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .width('100%')
        .height('100%')
        .onLoad(async () => {
          const surfaceId = this.xComponentController.getXComponentSurfaceId()
          await this.playerManager.init(surfaceId, { autoPlay: true })
          
          // 设置视频源(实际项目中替换为真实路径)
          const context = getContext(this) as common.UIAbilityContext
          await this.playerManager.setSourceAndPrepare(
            context.resourceDir + '/rawfile/sample.mp4'
          )
        })

      // 缓冲指示器
      if (this.bufferingPercent < 100 && this.playerState === PlayerState.PLAYING) {
        LoadingProgress()
          .width(48)
          .height(48)
          .color('#4FC3F7')
      }

      // 控制层(点击显示/隐藏)
      Column() {
        // 顶部信息栏
        Row() {
          Text('视频播放器')
            .fontSize(16)
            .fontColor('#ffffff')
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding(16)

        Blank()

        // 底部控制栏
        Column() {
          // 进度条
          Row() {
            Text(this.formatTime(this.currentTime))
              .fontSize(12)
              .fontColor('#ffffff')
              .width(50)
            
            Progress({
              value: this.currentTime,
              total: this.duration > 0 ? this.duration : 1,
              type: ProgressType.Linear
            })
              .width('60%')
              .color('#4FC3F7')
              .backgroundColor('#333333')
            
            Text(this.formatTime(this.duration))
              .fontSize(12)
              .fontColor('#ffffff')
              .width(50)
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)

          // 播放控制按钮
          Row() {
            Button('⏮')
              .onClick(() => this.playerManager.seek(Math.max(0, this.currentTime - 10000)))
            
            Button(this.playerState === PlayerState.PLAYING ? '⏸' : '▶')
              .onClick(() => {
                if (this.playerState === PlayerState.PLAYING) {
                  this.playerManager.pause()
                } else {
                  this.playerManager.play()
                }
              })
            
            Button('⏭')
              .onClick(() => this.playerManager.seek(Math.min(this.duration, this.currentTime + 10000)))
          }
          .justifyContent(FlexAlign.SpaceEvenly)
          .width('100%')
          .padding(8)
        }
        .width('100%')
        .padding(16)
        .backgroundColor('rgba(0,0,0,0.6)')
      }
      .width('100%')
      .height('100%')
      .visibility(this.showControls ? Visibility.Visible : Visibility.Hidden)
    }
    .width('100%')
    .height(300)
    .backgroundColor('#000000')
    .onClick(() => {
      this.showControls = !this.showControls
    })
  }

  private formatTime(ms: number): string {
    const totalSeconds = Math.floor(ms / 1000)
    const minutes = Math.floor(totalSeconds / 60)
    const seconds = totalSeconds % 60
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }
}

四、踩坑与注意事项

4.1 Surface绑定时机

:在AVPlayer进入initialized状态之前设置surfaceId无效,甚至可能导致崩溃。

:必须在stateChange回调中,当状态变为initialized时再设置surfaceId并调用prepare()。不要在创建AVPlayer后立即设置。

// ❌ 错误做法
this.avPlayer = await media.createAVPlayer()
this.avPlayer.surfaceId = surfaceId // 此时还在idle状态,设置无效!
this.avPlayer.url = videoUrl

// ✅ 正确做法
this.avPlayer.on('stateChange', (state) => {
  if (state === 'initialized') {
    this.avPlayer.surfaceId = surfaceId // 在initialized状态设置
    this.avPlayer.prepare()
  }
})
this.avPlayer.url = videoUrl

4.2 资源释放顺序

:页面退出时忘记释放AVPlayer,导致内存泄漏和后台持续播放。

:在aboutToDisappear中调用release(),且要先stop再release。

aboutToDisappear() {
  if (this.avPlayer) {
    // 先停止再释放
    if (this.playerState === 'playing' || this.playerState === 'paused') {
      this.avPlayer.stop()
    }
    this.avPlayer.release()
    this.avPlayer = null
  }
}

4.3 网络视频的权限配置

:播放网络视频时,没有配置网络权限,导致加载失败。

:在module.json5中添加网络权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

4.4 seek的精度问题

:调用seek()后,实际跳转的位置可能和目标位置有偏差,因为视频的关键帧间隔不是1秒。

:使用SeekMode控制跳转策略。SEEK_PREV_SYNC跳到目标位置之前最近的关键帧,SEEK_NEXT_SYNC跳到之后最近的关键帧。如果需要精确跳转,需要确保视频的关键帧间隔足够小。

4.5 后台播放处理

:应用切到后台后,视频播放可能被系统暂停。

:如果需要后台继续播放音频,需要申请长任务权限(ohos.permission.KEEP_BACKGROUND_RUNNING),并在Ability中配置backgroundModes。


五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
AVPlayer创建 media.createAVPlayer() 新增media.createAVPlayerSync()同步创建
HDR播放 不支持 新增HDR视频播放支持
画中画 需手动实现 新增@ohos.multimedia.pipWindow原生画中画API
视频解码 仅硬解 新增软件解码回退机制

5.2 迁移指南

// HarmonyOS 6 新增的画中画支持
import { pipWindow } from '@kit.MediaKit'

// 创建画中画控制器
let pipController: pipWindow.PipController | null = null

async function enterPipMode(avPlayer: media.AVPlayer) {
  try {
    // HarmonyOS 6 原生画中画
    pipController = await pipWindow.createPipController({
      componentId: 'videoSurface' // XComponent的ID
    })
    await pipController.startPip()
  } catch (err) {
    console.error(`进入画中画失败: ${err}`)
  }
}

5.3 性能提升

HarmonyOS 6对AVPlayer做了底层优化:

  • 启动速度:冷启动播放延迟降低约30%
  • 内存占用:优化了解码缓冲区管理,内存峰值降低约20%
  • HDR支持:新增对HDR10/HDR10+/Dolby Vision的播放支持

六、总结

mindmap
  root((视频播放))
    核心API
      Video组件
        声明式、开箱即用
        适合简单场景
        自动管理Surface
      AVPlayer
        底层、完全可控
        需手动管理状态机
        适合定制播放器
    状态机
      idle → initialized → prepared
      prepared → playing ↔ paused
      playing → completed
      any → stopped → idle
      any → error → idle
    关键要点
      Surface绑定时机
      资源及时释放
      网络权限配置
      seek精度控制
      后台播放处理
    封装实践
      VideoPlayerManager
      状态枚举管理
      配置接口抽象
      回调事件统一

一句话总结:视频播放的核心是理解状态机——先初始化、再设源、再准备、再播放,每一步都不能跳。封装播放器时,把状态管理和事件回调抽离出来,就能得到一个干净、可复用的播放器组件。

记住三个关键

  1. 状态机是灵魂——理解每个状态的含义和合法转换
  2. Surface是桥梁——AVPlayer必须绑定XComponent的Surface才能渲染画面
  3. 释放是底线——页面退出时务必释放播放器资源
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。