一起看看HarmonyOS开发中的视频编解码
核心要点:深入理解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):将压缩的码流还原为原始视频帧。

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效率优先)、合理配置参数(码率、帧率、关键帧间隔)、及时管理缓冲区,并在实际场景中持续监控性能指标。
记住三个关键:
- H.264保底,H.265提效——先检查设备是否支持H.265,不支持就回退H.264
- 缓冲区是生命线——不及时释放缓冲区,编解码器就会卡死
- 参数随场景变——直播用CBR+短关键帧间隔,存储用VBR+长关键帧间隔
- 点赞
- 收藏
- 关注作者
评论(0)