一起看看HarmonyOS开发中的视频编解码

举报
Jack20 发表于 2026/06/20 21:13:13 2026/06/20
【摘要】 核心要点:深入理解HarmonyOS视频编解码机制,掌握H.264/H.265编解码器的配置与使用,学会硬件编解码的调用方式,以及编解码参数调优和性能优化策略。 一、背景与动机你有没有想过,一段1分钟1080p的原始视频有多大?答案是大约3GB。而我们平时看到的1分钟1080p MP4视频通常只有30~50MB。这中间差了将近100倍的压缩,靠的就是视频编解码技术。编解码是视频处理的"心脏"...

核心要点:深入理解HarmonyOS视频编解码机制,掌握H.264/H.265编解码器的配置与使用,学会硬件编解码的调用方式,以及编解码参数调优和性能优化策略。


一、背景与动机

你有没有想过,一段1分钟1080p的原始视频有多大?答案是大约3GB。而我们平时看到的1分钟1080p MP4视频通常只有30~50MB。这中间差了将近100倍的压缩,靠的就是视频编解码技术。

编解码是视频处理的"心脏"——录制需要编码(把原始画面压缩),播放需要解码(把压缩数据还原),转码需要先解码再编码。理解编解码,你就掌握了视频开发的核心能力。

HarmonyOS提供了硬件编解码器(硬编硬解),利用芯片级的编解码单元,比软件编解码快10倍以上,功耗还低。但硬编硬解不是"无脑调用"——参数配置不当可能导致画面模糊、花屏、甚至编解码失败。

今天我们就来把视频编解码这件事掰开揉碎地讲清楚。


二、核心原理

2.1 编解码的基本概念

编码(Encoding/Compression):将原始视频帧(YUV/RGB数据)压缩为特定格式的码流(如H.264 Bitstream)。

解码(Decoding/Decompression):将压缩的码流还原为原始视频帧。
图片.png

2.2 H.264 vs H.265

特性 H.264 (AVC) H.265 (HEVC)
发布年份 2003 2013
压缩效率 基准 比H.264高约40~50%
计算复杂度 较低 比H.264高约3~5倍
兼容性 几乎所有设备 大部分新设备
适用场景 通用场景、直播 4K/8K、存储敏感场景
硬件支持 成熟 部分老设备不支持

选择建议

  • 如果需要最大兼容性,选H.264
  • 如果追求更小文件或更高画质,选H.265
  • 如果是4K及以上分辨率,强烈建议H.265

2.3 硬编硬解 vs 软编软解

flowchart TB
    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[编解码方式] --> B[硬件编解码]:::primary
    A --> C[软件编解码]:::warning
    
    B --> B1[✅ 速度快 10x+]:::info
    B --> B2[✅ 功耗低]:::info
    B --> B3[✅ 不占CPU]:::info
    B --> B4[❌ 设备兼容性限制]:::error
    B --> B5[❌ 参数配置受限]:::error
    
    C --> C1[✅ 兼容所有格式]:::info
    C --> C2[✅ 参数完全可控]:::info
    C --> C3[❌ 速度慢]:::error
    C --> C4[❌ 功耗高]:::error
    C --> C5[❌ 占用CPU]:::error

2.4 AVCodec状态机

HarmonyOS的编解码器(AVCodec)同样基于状态机:

stateDiagram-v2
    [*] --> idle : createByMime()
    idle --> configured : configure()
    configured --> running : start()
    running --> flushed : flush()
    flushed --> running : start()
    running --> stopped : stop()
    stopped --> configured : configure()
    stopped --> idle : reset()
    configured --> idle : reset()
    any --> error : 出错
    error --> idle : reset()

三、代码实战

3.1 基础实战:硬件视频解码

使用AVCodec进行硬件视频解码,将H.264码流解码为YUV帧:

// VideoDecoderDemo.ets
// 硬件视频解码示例

import { media } from '@kit.MediaKit'
import { fileIo as fs } from '@kit.CoreFileKit'
import { common } from '@kit.AbilityKit'

@Entry
@Component
struct VideoDecoderDemo {
  private videoDecoder: media.AVCodec | null = null
  private surfaceId: string = ''
  
  @State decoderState: string = 'idle'
  @State decodedFrames: number = 0
  @State statusMessage: string = '准备就绪'
  @State isDecoding: boolean = false

  aboutToDisappear() {
    this.releaseDecoder()
  }

  // 初始化视频解码器
  private async initDecoder(surfaceId: string) {
    this.surfaceId = surfaceId
    
    try {
      // 创建H.264硬件解码器
      this.videoDecoder = media.createVideoDecoderByMime(media.CodecMimeType.VIDEO_AVC)
      
      // 监听状态变化
      this.videoDecoder.on('stateChange', (state: string) => {
        this.decoderState = state
        console.info(`[Decoder] 状态: ${state}`)
      })

      // 监听解码输出 - 每解码一帧都会回调
      this.videoDecoder.on('newOutputData', (index: number, buffer: media.BufferInfo, flags: media.BufferFlag) => {
        this.decodedFrames++
        
        // 将解码后的帧渲染到Surface
        // 如果配置了outputSurface,解码帧会自动渲染
        
        // 释放输出缓冲区
        this.videoDecoder?.releaseOutputBuffer(index, true) // true表示渲染到Surface
      })

      // 监听需要输入数据的回调
      this.videoDecoder.on('needInputData', (index: number) => {
        // 填入编码数据(H.264码流)
        this.feedInputData(index)
      })

      // 监听错误
      this.videoDecoder.on('error', (err) => {
        this.statusMessage = `解码错误: ${err.message}`
        console.error(`[Decoder] 错误: ${JSON.stringify(err)}`)
      })

      // 配置解码器
      const config: media.VideoDecoderConfig = {
        mimeType: media.CodecMimeType.VIDEO_AVC,
        width: 1280,
        height: 720,
        pixelFormat: media.PixelFormat.YUV_420_P,
        // 输出Surface,解码后直接渲染
        outputSurface: this.surfaceId
      }

      await this.videoDecoder.configure(config)
      this.statusMessage = '解码器已配置'
      
    } catch (err) {
      this.statusMessage = `初始化失败: ${err}`
      console.error(`[Decoder] 初始化失败: ${err}`)
    }
  }

  // 填入编码数据
  private feedInputData(index: number) {
    if (!this.videoDecoder) return
    
    // 实际项目中,这里需要从文件或网络读取H.264码流数据
    // 这里演示填入空数据的逻辑框架
    const bufferInfo: media.BufferInfo = {
      offset: 0,
      size: 0, // 实际数据大小
      pts: 0,  // 时间戳
      flags: media.BufferFlag.BUFFER_FLAG_END_OF_STREAM // 结束标志
    }
    
    this.videoDecoder.queueInputBuffer(index, bufferInfo)
  }

  // 开始解码
  private async startDecoding() {
    if (!this.videoDecoder) return
    try {
      await this.videoDecoder.start()
      this.isDecoding = true
      this.statusMessage = '正在解码...'
    } catch (err) {
      this.statusMessage = `启动解码失败: ${err}`
    }
  }

  // 停止解码
  private async stopDecoding() {
    if (!this.videoDecoder) return
    try {
      await this.videoDecoder.stop()
      this.isDecoding = false
      this.statusMessage = '解码已停止'
    } catch (err) {
      this.statusMessage = `停止失败: ${err}`
    }
  }

  // 释放解码器
  private async releaseDecoder() {
    if (!this.videoDecoder) return
    try {
      if (this.isDecoding) {
        await this.videoDecoder.stop()
      }
      await this.videoDecoder.release()
      this.videoDecoder = null
    } catch (err) {
      console.error(`[Decoder] 释放失败: ${err}`)
    }
  }

  build() {
    Column() {
      // 解码输出渲染区域
      XComponent({
        id: 'decoderSurface',
        type: XComponentType.SURFACE,
        controller: new XComponentController()
      })
        .width('100%')
        .height(280)
        .onLoad(() => {
          // Surface加载后初始化解码器
        })

      // 状态信息
      Text(`状态: ${this.decoderState}`)
        .fontSize(14)
        .fontColor('#aaaaaa')
        .margin({ top: 8 })

      Text(this.statusMessage)
        .fontSize(16)
        .fontColor('#ffffff')
        .margin({ top: 4 })

      Text(`已解码帧数: ${this.decodedFrames}`)
        .fontSize(14)
        .fontColor('#4FC3F7')
        .margin({ top: 4 })

      // 控制按钮
      Row() {
        Button('初始化解码器')
          .onClick(() => this.initDecoder(this.surfaceId))

        Button('开始解码')
          .enabled(this.decoderState === 'configured')
          .onClick(() => this.startDecoding())

        Button('停止')
          .enabled(this.isDecoding)
          .onClick(() => this.stopDecoding())
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }
}

3.2 进阶实战:硬件视频编码

视频编码是将原始YUV帧编码为H.264/H.265码流,常用于录制和推流场景:

// VideoEncoderDemo.ets
// 硬件视频编码示例

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

// 编码配置接口
interface EncoderConfig {
  width: number
  height: number
  frameRate: number
  bitrate: number
  codecMime: media.CodecMimeType
  keyFrameInterval: number // 关键帧间隔(秒)
}

@Entry
@Component
struct VideoEncoderDemo {
  private videoEncoder: media.AVCodec | null = null
  
  @State encoderState: string = 'idle'
  @State encodedFrames: number = 0
  @State encodedSize: number = 0
  @State statusMessage: string = '准备就绪'
  @State isEncoding: boolean = false
  
  // 编码参数
  @State selectedResolution: number = 0
  @State selectedCodec: number = 0
  @State targetBitrate: number = 2000000

  // 预设分辨率
  private resolutions: { name: string; width: number; height: number }[] = [
    { name: '720p', width: 1280, height: 720 },
    { name: '1080p', width: 1920, height: 1080 },
    { name: '4K', width: 3840, height: 2160 }
  ]

  // 预设编码格式
  private codecs: { name: string; mime: media.CodecMimeType }[] = [
    { name: 'H.264', mime: media.CodecMimeType.VIDEO_AVC },
    { name: 'H.265', mime: media.CodecMimeType.VIDEO_HEVC }
  ]

  aboutToDisappear() {
    this.releaseEncoder()
  }

  // 初始化编码器
  private async initEncoder() {
    try {
      const codecMime = this.codecs[this.selectedCodec].mime
      
      // 创建硬件视频编码器
      this.videoEncoder = media.createVideoEncoderByMime(codecMime)
      
      // 监听状态变化
      this.videoEncoder.on('stateChange', (state: string) => {
        this.encoderState = state
        console.info(`[Encoder] 状态: ${state}`)
      })

      // 监听编码输出 - 编码后的数据通过此回调获取
      this.videoEncoder.on('newOutputData', (index: number, bufferInfo: media.BufferInfo, flags: media.BufferFlag) => {
        this.encodedFrames++
        this.encodedSize += bufferInfo.size
        
        // 检查是否是关键帧
        const isKeyFrame = (flags & media.BufferFlag.BUFFER_FLAG_KEY_FRAME) !== 0
        if (isKeyFrame) {
          console.info(`[Encoder] 关键帧 #${this.encodedFrames}`)
        }
        
        // 在实际项目中,这里需要将编码数据写入文件或发送到网络
        // this.writeEncodedData(index, bufferInfo)
        
        // 释放输出缓冲区
        this.videoEncoder?.releaseOutputBuffer(index)
      })

      // 监听需要输入数据的回调
      this.videoEncoder.on('needInputData', (index: number) => {
        // 填入待编码的YUV帧数据
        this.feedYUVFrame(index)
      })

      // 监听错误
      this.videoEncoder.on('error', (err) => {
        this.statusMessage = `编码错误: ${err.message}`
        console.error(`[Encoder] 错误: ${JSON.stringify(err)}`)
      })

      // 配置编码器
      const resolution = this.resolutions[this.selectedResolution]
      const config: media.VideoEncoderConfig = {
        mimeType: codecMime,
        width: resolution.width,
        height: resolution.height,
        frameRate: 30,
        bitrate: this.targetBitrate,
        pixelFormat: media.PixelFormat.YUV_420_P,
        // 关键帧间隔(IFrameInterval)
        // HarmonyOS 5.0+ 通过profile中的iFrameInterval配置
      }

      await this.videoEncoder.configure(config)
      this.statusMessage = `编码器已配置: ${resolution.name} ${this.codecs[this.selectedCodec].name}`
      
    } catch (err) {
      this.statusMessage = `初始化失败: ${err}`
      console.error(`[Encoder] 初始化失败: ${err}`)
    }
  }

  // 填入YUV帧数据
  private feedYUVFrame(index: number) {
    if (!this.videoEncoder) return
    
    // 实际项目中,这里需要从Camera或文件读取YUV帧数据
    // 这里演示填入数据的逻辑框架
    const bufferInfo: media.BufferInfo = {
      offset: 0,
      size: 0, // 实际YUV帧大小
      pts: this.encodedFrames * 33333, // 30fps下每帧约33ms
      flags: media.BufferFlag.BUFFER_FLAG_NONE
    }
    
    this.videoEncoder.queueInputBuffer(index, bufferInfo)
  }

  // 开始编码
  private async startEncoding() {
    if (!this.videoEncoder) return
    try {
      await this.videoEncoder.start()
      this.isEncoding = true
      this.statusMessage = '正在编码...'
    } catch (err) {
      this.statusMessage = `启动编码失败: ${err}`
    }
  }

  // 停止编码
  private async stopEncoding() {
    if (!this.videoEncoder) return
    try {
      await this.videoEncoder.stop()
      this.isEncoding = false
      this.statusMessage = `编码完成,共${this.encodedFrames}帧,${this.formatSize(this.encodedSize)}`
    } catch (err) {
      this.statusMessage = `停止失败: ${err}`
    }
  }

  // 释放编码器
  private async releaseEncoder() {
    if (!this.videoEncoder) return
    try {
      if (this.isEncoding) {
        await this.videoEncoder.stop()
      }
      await this.videoEncoder.release()
      this.videoEncoder = null
    } catch (err) {
      console.error(`[Encoder] 释放失败: ${err}`)
    }
  }

  build() {
    Column() {
      // 标题
      Text('视频编码器配置')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
        .margin({ bottom: 16 })

      // 分辨率选择
      Row() {
        Text('分辨率:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(60)
        
        ForEach(this.resolutions, (res: { name: string; width: number; height: number }, index: number) => {
          Button(res.name)
            .fontSize(12)
            .height(32)
            .backgroundColor(this.selectedResolution === index ? '#4FC3F7' : '#333333')
            .onClick(() => { this.selectedResolution = index })
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 12 })

      // 编码格式选择
      Row() {
        Text('编码:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(60)
        
        ForEach(this.codecs, (codec: { name: string; mime: media.CodecMimeType }, index: number) => {
          Button(codec.name)
            .fontSize(12)
            .height(32)
            .backgroundColor(this.selectedCodec === index ? '#4FC3F7' : '#333333')
            .onClick(() => { this.selectedCodec = index })
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 12 })

      // 码率控制
      Row() {
        Text('码率:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(60)
        
        Slider({
          value: this.targetBitrate / 1000,
          min: 500,
          max: 10000,
          step: 500
        })
          .width('50%')
          .onChange((value: number) => {
            this.targetBitrate = value * 1000
          })
        
        Text(`${this.targetBitrate / 1000}kbps`)
          .fontSize(14)
          .fontColor('#ffffff')
          .width(80)
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 16 })

      // 状态信息
      Text(this.statusMessage)
        .fontSize(16)
        .fontColor('#ffffff')
        .margin({ bottom: 8 })

      Text(`已编码: ${this.encodedFrames}帧 | ${this.formatSize(this.encodedSize)}`)
        .fontSize(14)
        .fontColor('#4FC3F7')
        .margin({ bottom: 16 })

      // 控制按钮
      Row() {
        Button('初始化')
          .onClick(() => this.initEncoder())

        Button('开始编码')
          .enabled(this.encoderState === 'configured')
          .backgroundColor('#4CAF50')
          .onClick(() => this.startEncoding())

        Button('停止')
          .enabled(this.isEncoding)
          .backgroundColor('#F44336')
          .onClick(() => this.stopEncoding())
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
    .justifyContent(FlexAlign.Center)
  }

  private formatSize(bytes: number): string {
    if (bytes < 1024) return `${bytes}B`
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
    return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
  }
}

3.3 高级实战:编解码参数优化与性能监控

在实际生产中,编解码参数的调优直接影响视频质量和性能。下面是一个完整的编解码参数优化和性能监控工具:

// CodecOptimizer.ets
// 编解码参数优化与性能监控

import { media } from '@kit.MediaKit'

// 编码质量预设
export enum QualityPreset {
  LOW = 'low',           // 低质量 - 最小文件
  MEDIUM = 'medium',     // 中等质量 - 平衡
  HIGH = 'high',         // 高质量 - 大文件
  LOSSLESS = 'lossless'  // 近无损 - 最大文件
}

// 编码参数配置
export interface CodecOptimizeConfig {
  width: number
  height: number
  frameRate: number
  quality: QualityPreset
  codec: media.CodecMimeType
  scene: 'streaming' | 'storage' | 'realtime' // 使用场景
}

// 性能统计数据
export interface CodecPerformanceStats {
  fps: number              // 实际编码帧率
  bitrateActual: number    // 实际码率
  encodeTimeMs: number     // 平均编码耗时
  droppedFrames: number    // 丢帧数
  memoryUsageMB: number    // 内存占用
  cpuUsagePercent: number  // CPU占用率
}

// 编解码优化器
export class CodecOptimizer {
  private encoder: media.AVCodec | null = null
  private stats: CodecPerformanceStats = {
    fps: 0, bitrateActual: 0, encodeTimeMs: 0,
    droppedFrames: 0, memoryUsageMB: 0, cpuUsagePercent: 0
  }
  private frameTimestamps: number[] = []
  private totalEncodeTime: number = 0
  private encodedFrameCount: number = 0
  private lastStatsTime: number = 0

  // 根据质量预设计算推荐码率
  static getRecommendedBitrate(config: CodecOptimizeConfig): number {
    const pixels = config.width * config.height
    const fps = config.frameRate
    
    // 基础码率 = 像素数 × 帧率 × 每像素比特数
    let bitsPerPixel: number
    
    switch (config.quality) {
      case QualityPreset.LOW:
        bitsPerPixel = 0.05
        break
      case QualityPreset.MEDIUM:
        bitsPerPixel = 0.1
        break
      case QualityPreset.HIGH:
        bitsPerPixel = 0.15
        break
      case QualityPreset.LOSSLESS:
        bitsPerPixel = 0.25
        break
    }

    // H.265比H.264效率高约40%
    if (config.codec === media.CodecMimeType.VIDEO_HEVC) {
      bitsPerPixel *= 0.6
    }

    // 场景调整
    switch (config.scene) {
      case 'streaming':
        bitsPerPixel *= 0.8 // 直播场景码率更保守
        break
      case 'realtime':
        bitsPerPixel *= 0.7 // 实时场景优先保证流畅
        break
      case 'storage':
        bitsPerPixel *= 1.2 // 存储场景可以更高码率
        break
    }

    return Math.round(pixels * fps * bitsPerPixel)
  }

  // 获取推荐的关键帧间隔
  static getKeyFrameInterval(scene: string): number {
    switch (scene) {
      case 'streaming':
        return 2 // 直播场景2秒一个关键帧,便于快进
      case 'realtime':
        return 3 // 实时场景3秒
      case 'storage':
        return 5 // 存储场景5秒,减少文件大小
      default:
        return 3
    }
  }

  // 获取推荐的编码配置
  static getOptimalConfig(config: CodecOptimizeConfig): media.VideoEncoderConfig {
    return {
      mimeType: config.codec,
      width: config.width,
      height: config.height,
      frameRate: config.frameRate,
      bitrate: CodecOptimizer.getRecommendedBitrate(config),
      pixelFormat: media.PixelFormat.YUV_420_P
    }
  }

  // 更新性能统计
  updateStats(encodeTimeMs: number) {
    const now = Date.now()
    this.totalEncodeTime += encodeTimeMs
    this.encodedFrameCount++
    this.frameTimestamps.push(now)

    // 只保留最近1秒的时间戳
    this.frameTimestamps = this.frameTimestamps.filter(t => now - t < 1000)
    this.stats.fps = this.frameTimestamps.length

    // 平均编码耗时
    this.stats.encodeTimeMs = this.encodedFrameCount > 0
      ? Math.round(this.totalEncodeTime / this.encodedFrameCount)
      : 0

    // 检测丢帧(如果编码耗时超过帧间隔,说明可能丢帧)
    const frameInterval = 1000 / 30 // 假设30fps
    if (encodeTimeMs > frameInterval) {
      this.stats.droppedFrames++
    }
  }

  // 获取当前性能统计
  getStats(): CodecPerformanceStats {
    return { ...this.stats }
  }

  // 重置统计
  resetStats() {
    this.stats = {
      fps: 0, bitrateActual: 0, encodeTimeMs: 0,
      droppedFrames: 0, memoryUsageMB: 0, cpuUsagePercent: 0
    }
    this.frameTimestamps = []
    this.totalEncodeTime = 0
    this.encodedFrameCount = 0
  }
}

// ====== 编解码优化器UI ======

@Entry
@Component
struct CodecOptimizerDemo {
  private optimizer: CodecOptimizer = new CodecOptimizer()
  
  @State selectedQuality: QualityPreset = QualityPreset.MEDIUM
  @State selectedScene: string = 'storage'
  @State selectedCodec: number = 0
  @State width: number = 1920
  @State height: number = 1080
  @State frameRate: number = 30
  @State recommendedBitrate: number = 0
  @State keyFrameInterval: number = 3
  @State stats: CodecPerformanceStats = {
    fps: 0, bitrateActual: 0, encodeTimeMs: 0,
    droppedFrames: 0, memoryUsageMB: 0, cpuUsagePercent: 0
  }

  aboutToAppear() {
    this.updateRecommendation()
  }

  // 更新推荐参数
  private updateRecommendation() {
    const config: CodecOptimizeConfig = {
      width: this.width,
      height: this.height,
      frameRate: this.frameRate,
      quality: this.selectedQuality,
      codec: this.selectedCodec === 0 ? media.CodecMimeType.VIDEO_AVC : media.CodecMimeType.VIDEO_HEVC,
      scene: this.selectedScene as 'streaming' | 'storage' | 'realtime'
    }

    this.recommendedBitrate = CodecOptimizer.getRecommendedBitrate(config)
    this.keyFrameInterval = CodecOptimizer.getKeyFrameInterval(config)
  }

  build() {
    Scroll() {
      Column() {
        // 标题
        Text('编解码参数优化器')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .margin({ bottom: 20 })

        // 分辨率配置
        Row() {
          Text('分辨率:')
            .fontSize(14)
            .fontColor('#aaaaaa')
            .width(70)
          
          TextInput({ text: this.width.toString() })
            .type(InputType.Number)
            .width(80)
            .height(36)
            .onChange((value: string) => {
              this.width = parseInt(value) || 1920
              this.updateRecommendation()
            })
          
          Text('×')
            .fontColor('#ffffff')
            .margin({ left: 4, right: 4 })
          
          TextInput({ text: this.height.toString() })
            .type(InputType.Number)
            .width(80)
            .height(36)
            .onChange((value: string) => {
              this.height = parseInt(value) || 1080
              this.updateRecommendation()
            })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
        .margin({ bottom: 12 })

        // 帧率
        Row() {
          Text('帧率:')
            .fontSize(14)
            .fontColor('#aaaaaa')
            .width(70)
          
          Slider({ value: this.frameRate, min: 15, max: 60, step: 5 })
            .width('55%')
            .onChange((value: number) => {
              this.frameRate = value
              this.updateRecommendation()
            })
          
          Text(`${this.frameRate}fps`)
            .fontSize(14)
            .fontColor('#ffffff')
            .width(60)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
        .margin({ bottom: 12 })

        // 编码格式
        Row() {
          Text('编码:')
            .fontSize(14)
            .fontColor('#aaaaaa')
            .width(70)
          
          Button('H.264')
            .fontSize(12)
            .height(32)
            .backgroundColor(this.selectedCodec === 0 ? '#4FC3F7' : '#333333')
            .onClick(() => { this.selectedCodec = 0; this.updateRecommendation() })
          
          Button('H.265')
            .fontSize(12)
            .height(32)
            .backgroundColor(this.selectedCodec === 1 ? '#4FC3F7' : '#333333')
            .onClick(() => { this.selectedCodec = 1; this.updateRecommendation() })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
        .margin({ bottom: 12 })

        // 质量预设
        Row() {
          Text('质量:')
            .fontSize(14)
            .fontColor('#aaaaaa')
            .width(70)
          
          ForEach([
            { key: QualityPreset.LOW, label: '低' },
            { key: QualityPreset.MEDIUM, label: '中' },
            { key: QualityPreset.HIGH, label: '高' },
            { key: QualityPreset.LOSSLESS, label: '无损' }
          ], (item: { key: QualityPreset; label: string }) => {
            Button(item.label)
              .fontSize(12)
              .height(32)
              .backgroundColor(this.selectedQuality === item.key ? '#4FC3F7' : '#333333')
              .onClick(() => { this.selectedQuality = item.key; this.updateRecommendation() })
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
        .margin({ bottom: 12 })

        // 使用场景
        Row() {
          Text('场景:')
            .fontSize(14)
            .fontColor('#aaaaaa')
            .width(70)
          
          ForEach([
            { key: 'streaming', label: '直播' },
            { key: 'storage', label: '存储' },
            { key: 'realtime', label: '实时' }
          ], (item: { key: string; label: string }) => {
            Button(item.label)
              .fontSize(12)
              .height(32)
              .backgroundColor(this.selectedScene === item.key ? '#4FC3F7' : '#333333')
              .onClick(() => { this.selectedScene = item.key; this.updateRecommendation() })
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
        .margin({ bottom: 20 })

        // 推荐参数展示
        Column() {
          Text('推荐参数')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4FC3F7')
            .margin({ bottom: 12 })

          Row() {
            Text('推荐码率:')
              .fontSize(14)
              .fontColor('#aaaaaa')
            Text(`${(this.recommendedBitrate / 1000).toFixed(0)} kbps`)
              .fontSize(14)
              .fontColor('#ffffff')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .margin({ bottom: 8 })

          Row() {
            Text('关键帧间隔:')
              .fontSize(14)
              .fontColor('#aaaaaa')
            Text(`${this.keyFrameInterval}`)
              .fontSize(14)
              .fontColor('#ffffff')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .margin({ bottom: 8 })

          Row() {
            Text('预估文件大小(1分钟):')
              .fontSize(14)
              .fontColor('#aaaaaa')
            Text(`${(this.recommendedBitrate * 60 / 8 / 1024 / 1024).toFixed(1)} MB`)
              .fontSize(14)
              .fontColor('#ffffff')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
        }
        .width('90%')
        .padding(16)
        .borderRadius(12)
        .backgroundColor('rgba(79, 195, 247, 0.1)')
        .border({ width: 1, color: 'rgba(79, 195, 247, 0.3)' })

        // 性能监控
        Column() {
          Text('性能监控')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#81C784')
            .margin({ bottom: 12 })

          Text(`编码帧率: ${this.stats.fps} fps`)
            .fontSize(14)
            .fontColor('#ffffff')
            .margin({ bottom: 4 })
          
          Text(`平均编码耗时: ${this.stats.encodeTimeMs} ms`)
            .fontSize(14)
            .fontColor('#ffffff')
            .margin({ bottom: 4 })
          
          Text(`丢帧数: ${this.stats.droppedFrames}`)
            .fontSize(14)
            .fontColor(this.stats.droppedFrames > 0 ? '#EF5350' : '#ffffff')
            .margin({ bottom: 4 })
        }
        .width('90%')
        .padding(16)
        .borderRadius(12)
        .backgroundColor('rgba(129, 199, 132, 0.1)')
        .border({ width: 1, color: 'rgba(129, 199, 132, 0.3)' })
        .margin({ top: 16 })
      }
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }
}

四、踩坑与注意事项

4.1 硬件编解码器兼容性

:不是所有设备都支持H.265硬件编解码,老设备可能只有H.264硬解。

:在使用前检查设备能力:

// 检查是否支持H.265硬件编码
import { media } from '@kit.MediaKit'

function isHEVCSupported(): boolean {
  try {
    // 尝试创建H.265编码器
    const encoder = media.createVideoEncoderByMime(media.CodecMimeType.VIDEO_HEVC)
    if (encoder) {
      // 创建成功,说明支持
      return true
    }
  } catch (err) {
    // 创建失败,不支持H.265
    console.info('设备不支持H.265硬件编码')
  }
  return false
}

4.2 缓冲区管理

:编解码器的输入输出缓冲区是有限的,不及时释放会导致编解码器卡住。

  • newOutputData回调中,处理完数据后必须调用releaseOutputBuffer()
  • needInputData回调中,填入数据后必须调用queueInputBuffer()
  • 不要在回调中做耗时操作,否则会阻塞编解码管线

4.3 关键帧间隔设置

:关键帧间隔太大,导致seek操作不精确;间隔太小,文件体积增大。

:根据使用场景选择合适的关键帧间隔:

  • 直播/流媒体:2秒(1~2秒一个关键帧,便于快速seek)
  • 本地存储:3~5秒(减少文件大小)
  • 视频编辑:1秒(精确到帧的编辑需要频繁的关键帧)

4.4 码率控制模式

:固定码率(CBR)在复杂场景下画质下降明显,可变码率(VBR)在简单场景下浪费带宽。

  • CBR(固定码率):适合直播推流,带宽可控
  • VBR(可变码率):适合本地存储,画质优先
  • ABR(平均码率):折中方案,适合大多数场景

HarmonyOS的AVCodec默认使用ABR模式,可以通过配置参数调整。

4.5 内存对齐要求

:硬件编解码器对输入数据有内存对齐要求(通常是16或32字节对齐),不对齐的数据可能导致编解码失败或画面异常。

:确保输入的YUV帧数据的宽高是16的倍数(常见分辨率如1280×720、1920×1080都满足)。如果源数据不是16对齐的,需要做padding处理。


五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
AVCodec创建 createVideoDecoderByMime() 新增createVideoDecoderByName()指定编码器名称
低延迟解码 不支持 新增低延迟解码模式
码率动态调整 仅录制时支持 编码过程中也可动态调整
HDR编解码 不支持 新增HDR10/HLG编解码支持
10-bit色彩 不支持 新增10-bit YUV输入支持

5.2 迁移指南

// HarmonyOS 6 新增的低延迟解码模式
const decoderConfig: media.VideoDecoderConfig = {
  mimeType: media.CodecMimeType.VIDEO_AVC,
  width: 1920,
  height: 1080,
  pixelFormat: media.PixelFormat.YUV_420_P,
  outputSurface: surfaceId,
  // HarmonyOS 6 新增:低延迟配置
  // lowLatency: true  // 启用低延迟解码,适合实时通信场景
}

5.3 性能提升

HarmonyOS 6对编解码性能做了显著优化:

  • H.265编码速度:提升约20%,得益于新的硬件编码器驱动
  • 4K解码:功耗降低约15%
  • 多实例并发:支持同时运行更多编解码实例

六、总结

mindmap
  root((视频编解码))
    编码格式
      H.264 AVC
        兼容性最好
        压缩效率一般
      H.265 HEVC
        压缩效率高40%
        4K首选
    硬件vs软件
      硬件编解码
        速度快10x+
        功耗低
        兼容性受限
      软件编解码
        兼容性好
        速度慢
        功耗高
    参数优化
      码率控制
        CBR/VBR/ABR
      关键帧间隔
        直播2s/存储5s
      质量预设
        低///无损
    性能监控
      编码帧率
      编码耗时
      丢帧检测
      内存占用
    关键要点
      设备兼容性检查
      缓冲区及时释放
      内存对齐要求
      码率模式选择

一句话总结:视频编解码的核心是选择合适的编码格式(H.264兼容优先、H.265效率优先)、合理配置参数(码率、帧率、关键帧间隔)、及时管理缓冲区,并在实际场景中持续监控性能指标。

记住三个关键

  1. H.264保底,H.265提效——先检查设备是否支持H.265,不支持就回退H.264
  2. 缓冲区是生命线——不及时释放缓冲区,编解码器就会卡死
  3. 参数随场景变——直播用CBR+短关键帧间隔,存储用VBR+长关键帧间隔
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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