HarmonyOS开发视频缩略图
HarmonyOS开发中的视频缩略图:@ohos.multimedia.media、缩略图提取、时间点截图、缩略图缓存、批量提取
核心要点:掌握HarmonyOS视频缩略图提取全流程,从单帧截图到批量提取,从内存缓存到磁盘缓存,打造高效流畅的视频预览体验。
一、背景与动机
你打开手机相册,看到一堆视频文件,每个视频下面都有一张预览图——这就是视频缩略图。看起来简单,但背后大有学问。
想象一下,如果你的视频列表有100个视频,每个视频都要实时解码提取缩略图,那页面加载得等到猴年马月?用户早就失去耐心了。所以,缩略图的提取时机、缓存策略、批量处理效率,每一个环节都直接影响用户体验。
在HarmonyOS上,提取视频缩略图有两种主要方式:AVImageGenerator(专门用于提取视频帧图像)和mediaLibrary(媒体库提供的缩略图接口)。前者灵活可控,可以指定任意时间点提取;后者简单快捷,但只能获取系统默认缩略图。
本文就来把视频缩略图这件事掰开揉碎讲清楚。
二、核心原理
2.1 视频缩略图提取方式对比
flowchart TD
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[AVImageGenerator]:::warning
A --> C[mediaLibrary]:::info
A --> D[AVMetadataExtractor]:::purple
B --> B1[指定时间点提取]:::primary
B --> B2[批量帧提取]:::primary
B --> B3[自定义分辨率]:::primary
B --> B4[精确到毫秒]:::primary
C --> C1[系统默认缩略图]:::info
C --> C2[快速获取]:::info
C --> C3[无需解码]:::info
D --> D1[元数据提取]:::purple
D --> D2[封面图提取]:::purple
style A stroke-width:3px
style B stroke-width:3px
2.2 AVImageGenerator工作原理
AVImageGenerator是HarmonyOS提供的视频帧图像提取器,其核心工作流程:
- 创建实例:通过
media.createAVImageGenerator()创建 - 设置数据源:配置
fdSrc(文件描述符)或url(文件路径) - 请求帧数据:调用
fetchFrameByTime()指定时间点获取图像 - 释放资源:使用完毕后调用
release()释放
关键参数说明:
- timeMs:目标时间点(毫秒)
- options:提取选项,包含
framePixelMap(是否返回PixelMap)和`colorSpace**(色彩空间)
2.3 缩略图缓存架构
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{内存缓存命中?}:::warning
B -->|命中| C[返回内存缓存]:::info
B -->|未命中| D{磁盘缓存命中?}:::purple
D -->|命中| E[读取磁盘缓存]:::info
D -->|未命中| F[AVImageGenerator提取]:::error
F --> G[写入磁盘缓存]:::purple
G --> H[写入内存缓存]:::primary
E --> H
H --> I[返回缩略图]:::info
style B stroke-width:3px
style D stroke-width:3px
缓存策略的核心思想是LRU(最近最少使用):
- 内存缓存:使用Map存储PixelMap,容量有限(建议不超过50MB)
- 磁盘缓存:存储压缩后的图片文件,容量较大(建议不超过500MB)
- 缓存Key:
视频路径_时间点_分辨率组合,确保唯一性
三、代码实战
3.1 基础缩略图提取
使用AVImageGenerator从视频中提取指定时间点的帧图像。
import { media } from '@kit.MediaKit';
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 视频缩略图提取器
* 支持从视频中提取指定时间点的帧图像
*/
class VideoThumbnailExtractor {
private avImageGenerator: media.AVImageGenerator | null = null;
/**
* 从视频中提取指定时间点的缩略图
* @param videoPath 视频文件路径
* @param timeMs 目标时间点(毫秒)
* @param width 缩略图宽度(可选,默认320)
* @param height 缩略图高度(可选,默认180)
* @returns PixelMap格式的缩略图
*/
async extractThumbnail(
videoPath: string,
timeMs: number,
width: number = 320,
height: number = 180
): Promise<image.PixelMap | null> {
try {
// 第一步:创建AVImageGenerator实例
this.avImageGenerator = await media.createAVImageGenerator();
console.info('[Thumbnail] AVImageGenerator创建成功');
// 第二步:设置视频数据源(使用文件描述符方式)
const fileDescriptor = await fileIo.open(videoPath, fileIo.OpenMode.READ_ONLY);
this.avImageGenerator.fdSrc = fileDescriptor;
// 第三步:配置提取参数
const queryOption: media.AVImageQueryOptions = {
timeMicros: timeMs * 1000, // 转换为微秒
};
const pixelMapParams: media.PixelMapParams = {
width: width,
height: height,
};
// 第四步:提取帧图像
const pixelMap = await this.avImageGenerator.fetchFrameByTime(
queryOption.timeMicros,
media.AVImageQueryOptions.FRAME_BY_TIME, // 精确时间点模式
pixelMapParams
);
console.info(`[Thumbnail] 缩略图提取成功: ${width}x${height}, 时间点=${timeMs}ms`);
return pixelMap;
} catch (error) {
const err = error as BusinessError;
console.error(`[Thumbnail] 提取失败: ${err.code} - ${err.message}`);
return null;
} finally {
// 第五步:释放资源
await this.release();
}
}
/**
* 从视频中提取封面图(第一帧)
* @param videoPath 视频文件路径
* @returns 封面图PixelMap
*/
async extractCover(videoPath: string): Promise<image.PixelMap | null> {
return this.extractThumbnail(videoPath, 0, 640, 360);
}
/**
* 释放AVImageGenerator资源
*/
private async release(): Promise<void> {
if (this.avImageGenerator) {
try {
await this.avImageGenerator.release();
} catch (e) {
// 忽略释放错误
}
this.avImageGenerator = null;
}
}
}
// 使用示例
async function demoBasicThumbnail(): Promise<void> {
const extractor = new VideoThumbnailExtractor();
// 提取视频第5秒的缩略图
const thumbnail = await extractor.extractThumbnail(
'/data/storage/el2/base/haps/entry/files/sample.mp4',
5000, // 5秒
320,
180
);
if (thumbnail) {
console.info('缩略图提取成功,可用于UI展示');
}
}
3.2 缩略图缓存管理器
直接每次都从视频解码提取缩略图,性能开销太大。必须引入缓存机制——先查内存,再查磁盘,都没有才解码提取。
import { media } from '@kit.MediaKit';
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 缩略图缓存管理器
* 实现内存缓存 + 磁盘缓存的两级缓存策略
*/
class ThumbnailCacheManager {
// 内存缓存:LRU策略,存储PixelMap
private memoryCache: Map<string, image.PixelMap> = new Map();
private maxMemoryCacheSize: number = 50; // 最多缓存50个缩略图
// 磁盘缓存目录
private diskCacheDir: string = '/data/storage/el2/base/cache/thumbnails/';
// AVImageGenerator实例(复用,避免频繁创建销毁)
private avImageGenerator: media.AVImageGenerator | null = null;
/**
* 获取缩略图(带缓存)
* 查找顺序:内存缓存 → 磁盘缓存 → 实时提取
*/
async getThumbnail(
videoPath: string,
timeMs: number,
width: number = 320,
height: number = 180
): Promise<image.PixelMap | null> {
// 生成缓存Key
const cacheKey = this.buildCacheKey(videoPath, timeMs, width, height);
// 第一步:查找内存缓存
const memCached = this.memoryCache.get(cacheKey);
if (memCached) {
console.info(`[Cache] 内存缓存命中: ${cacheKey}`);
return memCached;
}
// 第二步:查找磁盘缓存
const diskCached = await this.loadFromDiskCache(cacheKey);
if (diskCached) {
console.info(`[Cache] 磁盘缓存命中: ${cacheKey}`);
// 回填内存缓存
this.putMemoryCache(cacheKey, diskCached);
return diskCached;
}
// 第三步:实时提取
console.info(`[Cache] 缓存未命中,实时提取: ${cacheKey}`);
const extracted = await this.extractFromVideo(videoPath, timeMs, width, height);
if (extracted) {
// 写入磁盘缓存
await this.saveToDiskCache(cacheKey, extracted);
// 写入内存缓存
this.putMemoryCache(cacheKey, extracted);
}
return extracted;
}
/**
* 构建缓存Key
* 格式:视频文件名_时间点_宽x高
*/
private buildCacheKey(videoPath: string, timeMs: number, width: number, height: number): string {
const fileName = videoPath.split('/').pop() || 'unknown';
return `${fileName}_${timeMs}ms_${width}x${height}`;
}
/**
* 写入内存缓存(LRU淘汰)
*/
private putMemoryCache(key: string, pixelMap: image.PixelMap): void {
// 如果超过最大缓存数量,淘汰最早的条目
if (this.memoryCache.size >= this.maxMemoryCacheSize) {
const oldestKey = this.memoryCache.keys().next().value;
if (oldestKey) {
const oldPixelMap = this.memoryCache.get(oldestKey);
oldPixelMap?.release(); // 释放PixelMap资源
this.memoryCache.delete(oldestKey);
}
}
this.memoryCache.set(key, pixelMap);
}
/**
* 从磁盘缓存加载
*/
private async loadFromDiskCache(cacheKey: string): Promise<image.PixelMap | null> {
try {
const cacheFilePath = `${this.diskCacheDir}${cacheKey}.jpg`;
const exists = await this.fileExists(cacheFilePath);
if (!exists) {
return null;
}
// 读取图片文件并解码为PixelMap
const imageSource = image.createImageSource(cacheFilePath);
const pixelMap = await imageSource.createPixelMap();
await imageSource.release();
return pixelMap;
} catch (error) {
return null;
}
}
/**
* 保存到磁盘缓存
*/
private async saveToDiskCache(cacheKey: string, pixelMap: image.PixelMap): Promise<void> {
try {
// 确保缓存目录存在
await this.ensureDir(this.diskCacheDir);
const cacheFilePath = `${this.diskCacheDir}${cacheKey}.jpg`;
// PixelMap打包为JPEG数据
const packer = image.createImagePacker();
const packOpts: image.PackingOption = {
format: 'image/jpeg',
quality: 85, // JPEG质量85%
};
const packData = await packer.packing(pixelMap, packOpts);
const dataBuffer = packData.getPixels();
// 写入文件
const file = await fileIo.open(cacheFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
await fileIo.write(file.fd, dataBuffer.buffer);
await fileIo.close(file.fd);
await packer.release();
packData.release();
} catch (error) {
console.warn(`[Cache] 磁盘缓存写入失败: ${(error as BusinessError).message}`);
}
}
/**
* 从视频中提取缩略图
*/
private async extractFromVideo(
videoPath: string,
timeMs: number,
width: number,
height: number
): Promise<image.PixelMap | null> {
try {
// 复用AVImageGenerator实例
if (!this.avImageGenerator) {
this.avImageGenerator = await media.createAVImageGenerator();
}
// 设置数据源
const fileDescriptor = await fileIo.open(videoPath, fileIo.OpenMode.READ_ONLY);
this.avImageGenerator.fdSrc = fileDescriptor;
// 提取帧
const pixelMap = await this.avImageGenerator.fetchFrameByTime(
timeMs * 1000, // 微秒
media.AVImageQueryOptions.FRAME_BY_TIME,
{ width, height }
);
return pixelMap;
} catch (error) {
const err = error as BusinessError;
console.error(`[Cache] 提取失败: ${err.code} - ${err.message}`);
return null;
}
}
/**
* 检查文件是否存在
*/
private async fileExists(path: string): Promise<boolean> {
try {
await fileIo.access(path);
return true;
} catch {
return false;
}
}
/**
* 确保目录存在
*/
private async ensureDir(dirPath: string): Promise<void> {
try {
await fileIo.mkdir(dirPath);
} catch {
// 目录已存在,忽略
}
}
/**
* 清除所有缓存
*/
async clearAllCache(): Promise<void> {
// 清除内存缓存
for (const [, pixelMap] of this.memoryCache) {
pixelMap.release();
}
this.memoryCache.clear();
// 清除磁盘缓存
try {
await fileIo.rmdir(this.diskCacheDir);
await this.ensureDir(this.diskCacheDir);
} catch {
// 忽略
}
console.info('[Cache] 所有缓存已清除');
}
/**
* 获取缓存统计信息
*/
getCacheStats(): { memoryCount: number; memorySizeKB: number } {
return {
memoryCount: this.memoryCache.size,
memorySizeKB: this.memoryCache.size * 320 * 180 * 4 / 1024, // 估算
};
}
/**
* 释放资源
*/
async release(): Promise<void> {
// 释放内存缓存中的PixelMap
for (const [, pixelMap] of this.memoryCache) {
pixelMap.release();
}
this.memoryCache.clear();
// 释放AVImageGenerator
if (this.avImageGenerator) {
try {
await this.avImageGenerator.release();
} catch (e) {
// 忽略
}
this.avImageGenerator = null;
}
}
}
3.3 批量缩略图提取与视频预览条
视频播放器常见的功能:拖动进度条时显示预览缩略图。这需要批量提取视频的多个时间点缩略图。
import { media } from '@kit.MediaKit';
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 批量缩略图提取器
* 用于视频进度条预览、视频列表缩略图等场景
*/
class BatchThumbnailExtractor {
private avImageGenerator: media.AVImageGenerator | null = null;
private cacheManager: ThumbnailCacheManager | null = null;
constructor() {
this.cacheManager = new ThumbnailCacheManager();
}
/**
* 批量提取视频缩略图
* 按等间隔时间点提取多帧缩略图
* @param videoPath 视频文件路径
* @param count 需要提取的缩略图数量
* @param width 单张缩略图宽度
* @param height 单张缩略图高度
* @returns 按时间排序的缩略图数组
*/
async extractBatchThumbnails(
videoPath: string,
count: number = 10,
width: number = 160,
height: number = 90
): Promise<ThumbnailItem[]> {
const thumbnails: ThumbnailItem[] = [];
try {
// 第一步:获取视频时长
const duration = await this.getVideoDuration(videoPath);
if (duration <= 0) {
console.error('[Batch] 无效的视频时长');
return thumbnails;
}
console.info(`[Batch] 视频时长: ${duration}ms, 提取${count}张缩略图`);
// 第二步:计算等间隔时间点
const interval = duration / (count + 1); // 避免首尾帧
// 第三步:逐帧提取(带缓存)
for (let i = 1; i <= count; i++) {
const timeMs = Math.floor(interval * i);
const pixelMap = await this.cacheManager?.getThumbnail(
videoPath, timeMs, width, height
);
thumbnails.push({
timeMs: timeMs,
pixelMap: pixelMap,
index: i - 1,
});
// 每提取5帧打印一次进度
if (i % 5 === 0) {
console.info(`[Batch] 已提取 ${i}/${count} 帧`);
}
}
console.info(`[Batch] 批量提取完成,共${thumbnails.length}帧`);
} catch (error) {
const err = error as BusinessError;
console.error(`[Batch] 批量提取失败: ${err.code} - ${err.message}`);
}
return thumbnails;
}
/**
* 提取视频进度条预览缩略图
* 专门用于播放器进度条拖动预览
* @param videoPath 视频文件路径
* @param segmentCount 分段数量(建议10-20)
* @returns 时间点与缩略图的映射
*/
async extractSeekBarThumbnails(
videoPath: string,
segmentCount: number = 15
): Promise<Map<number, image.PixelMap>> {
const thumbnailMap = new Map<number, image.PixelMap>();
try {
const duration = await this.getVideoDuration(videoPath);
if (duration <= 0) {
return thumbnailMap;
}
// 每段的时间长度
const segmentDuration = Math.floor(duration / segmentCount);
for (let i = 0; i < segmentCount; i++) {
const timeMs = segmentDuration * i;
const pixelMap = await this.cacheManager?.getThumbnail(
videoPath, timeMs, 160, 90
);
if (pixelMap) {
thumbnailMap.set(timeMs, pixelMap);
}
}
console.info(`[SeekBar] 进度条预览缩略图提取完成: ${thumbnailMap.size}/${segmentCount}`);
} catch (error) {
console.error('[SeekBar] 提取失败');
}
return thumbnailMap;
}
/**
* 获取视频时长
*/
private async getVideoDuration(videoPath: string): Promise<number> {
try {
this.avImageGenerator = await media.createAVImageGenerator();
const fileDescriptor = await fileIo.open(videoPath, fileIo.OpenMode.READ_ONLY);
this.avImageGenerator.fdSrc = fileDescriptor;
// 获取视频元数据中的时长信息
const duration = this.avImageGenerator.duration;
return duration; // 返回毫秒
} catch (error) {
console.error('[Batch] 获取视频时长失败');
return 0;
}
}
/**
* 释放资源
*/
async release(): Promise<void> {
if (this.avImageGenerator) {
try {
await this.avImageGenerator.release();
} catch (e) {
// 忽略
}
this.avImageGenerator = null;
}
await this.cacheManager?.release();
}
}
/**
* 缩略图条目
*/
interface ThumbnailItem {
timeMs: number; // 时间点(毫秒)
pixelMap: image.PixelMap | null; // 缩略图数据
index: number; // 序号
}
/**
* 视频列表缩略图组件
* 演示批量缩略图在UI中的使用
*/
@Component
struct VideoThumbnailGrid {
@State thumbnailList: ThumbnailItem[] = [];
@State isLoading: boolean = false;
private extractor: BatchThumbnailExtractor = new BatchThumbnailExtractor();
build() {
Column() {
// 加载状态提示
if (this.isLoading) {
LoadingProgress()
.width(40)
.height(40)
.color('#4FC3F7')
Text('正在提取缩略图...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
}
// 缩略图网格
Grid() {
ForEach(this.thumbnailList, (item: ThumbnailItem, index: number) => {
GridItem() {
Column() {
if (item.pixelMap) {
Image(item.pixelMap)
.width(160)
.height(90)
.objectFit(ImageFit.Cover)
.borderRadius(4)
} else {
// 占位图
Column() {
Text(`${(item.timeMs / 1000).toFixed(1)}s`)
.fontSize(12)
.fontColor('#FFFFFF')
}
.width(160)
.height(90)
.backgroundColor('#333333')
.borderRadius(4)
.justifyContent(FlexAlign.Center)
}
// 时间标签
Text(this.formatTime(item.timeMs))
.fontSize(10)
.fontColor('#AAAAAA')
.margin({ top: 4 })
}
}
}, (item: ThumbnailItem, index: number) => `${index}`)
}
.columnsTemplate('1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#1A1A2E')
}
/**
* 加载视频缩略图
*/
async loadThumbnails(videoPath: string): Promise<void> {
this.isLoading = true;
this.thumbnailList = await this.extractor.extractBatchThumbnails(
videoPath, 10, 160, 90
);
this.isLoading = false;
}
/**
* 格式化时间显示
*/
private formatTime(timeMs: number): string {
const seconds = Math.floor(timeMs / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
}
四、踩坑与注意事项
4.1 AVImageGenerator使用陷阱
| 坑点 | 现象 | 解决方案 |
|---|---|---|
| 未释放fdSrc | 文件描述符泄漏,导致文件被占用 | 提取完毕后及时关闭fd |
| 时间点超出范围 | 返回最后一帧或null | 先获取duration,确保timeMs在有效范围内 |
| PixelMap未释放 | 内存持续增长 | 使用完毕后调用pixelMap.release() |
| 并发提取过多 | CPU/GPU过载,提取变慢 | 控制并发数,建议不超过3个同时提取 |
| 缩略图尺寸过大 | 解码耗时、内存占用高 | 列表场景用160x90,详情页用640x360 |
4.2 缓存策略注意事项
问题1:缓存Key冲突
如果两个不同的视频恰好文件名相同(不同路径),会导致缓存冲突。解决方案:在Key中加入文件大小或修改时间。
// 改进的缓存Key生成
private async buildCacheKeyEnhanced(videoPath: string, timeMs: number): Promise<string> {
const stat = await fileIo.stat(videoPath);
const fileName = videoPath.split('/').pop() || 'unknown';
return `${fileName}_${stat.size}_${timeMs}ms`;
}
问题2:磁盘缓存无限增长
必须设置磁盘缓存上限,定期清理。建议:
- 最大缓存空间:500MB
- 清理策略:LRU + 超时(7天未访问自动清理)
问题3:PixelMap生命周期管理
PixelMap是Native资源,不释放会导致内存泄漏。特别注意:
- 列表滚动时,离开视口的PixelMap要释放
- 缓存淘汰时,对应的PixelMap要释放
- 页面销毁时,所有PixelMap要释放
4.3 性能优化建议
- 预提取:在视频列表加载时,提前提取前几页的缩略图
- 降级策略:提取失败时使用视频第一帧或默认占位图
- 并发控制:使用任务队列控制同时提取的数量
- 分辨率适配:根据UI显示尺寸选择合适的缩略图分辨率,不要提取过大
五、HarmonyOS 6适配
5.1 API变更
| 变更项 | HarmonyOS 5.0 | HarmonyOS 6 |
|---|---|---|
| AVImageGenerator | 基础帧提取 | 新增fetchFramesByTimeRange批量接口 |
| 缩略图质量 | 固定质量 | 新增quality参数,支持快速模式 |
| HDR缩略图 | 不支持 | 支持HDR视频的缩略图提取 |
| 缓存管理 | 无内置缓存 | 新增ThumbnailCache系统级缓存 |
| 并发提取 | 单线程 | 支持多线程并发提取 |
5.2 迁移指南
// HarmonyOS 6 新增的时间范围批量提取
import { media } from '@kit.MediaKit';
async function extractByTimeRange(videoPath: string): Promise<void> {
const generator = await media.createAVImageGenerator();
const fd = await fileIo.open(videoPath, fileIo.OpenMode.READ_ONLY);
generator.fdSrc = fd;
// HarmonyOS 6: 一次性提取时间范围内的多帧
const frames = await generator.fetchFramesByTimeRange({
startTimeMicros: 0, // 起始时间(微秒)
endTimeMicros: 60000000, // 结束时间(60秒)
frameCount: 10, // 提取10帧
quality: 'fast', // 快速模式,优先速度
pixelMapParams: {
width: 160,
height: 90,
}
});
console.info(`提取了${frames.length}帧缩略图`);
await generator.release();
}
5.3 HarmonyOS 6系统级缩略图缓存
// HarmonyOS 6 新增的 ThumbnailCache API
import { media } from '@kit.MediaKit';
async function useSystemThumbnailCache(videoPath: string): Promise<image.PixelMap | null> {
// 系统级缓存,跨应用共享,自动管理
const thumbnailCache = media.getThumbnailCache();
const pixelMap = await thumbnailCache.get(videoPath, {
timeMs: 5000,
width: 320,
height: 180,
});
return pixelMap;
}
六、总结
mindmap
root((视频缩略图))
提取方式
AVImageGenerator
指定时间点提取
自定义分辨率
精确到微秒
mediaLibrary
系统默认缩略图
快速获取
AVMetadataExtractor
元数据封面图
缓存策略
内存缓存
LRU淘汰
50条上限
PixelMap管理
磁盘缓存
JPEG压缩存储
500MB上限
超时清理
缓存Key设计
路径+时间+分辨率
文件大小防冲突
批量提取
等间隔提取
进度条预览
并发控制
任务队列
性能优化
预提取策略
降级占位图
分辨率适配
PixelMap释放
HarmonyOS 6
时间范围批量提取
快速模式
HDR缩略图
系统级缓存
关键要点回顾:
- AVImageGenerator是核心:灵活可控,支持指定时间点和分辨率提取,是缩略图提取的首选方案
- 缓存是必须的:没有缓存的缩略图系统就是性能灾难,内存+磁盘两级缓存缺一不可
- PixelMap生命周期:这是最容易踩的坑,忘记释放PixelMap会导致内存泄漏,必须严格管理
- 批量提取要控并发:同时提取太多帧会导致CPU/GPU过载,建议并发数不超过3
- HarmonyOS 6大幅增强:批量接口、快速模式、系统级缓存,开发效率和使用体验都有质的提升
下一篇我们将深入字幕渲染技术,看看如何解析SRT/ASS字幕并实现精确同步渲染。
- 点赞
- 收藏
- 关注作者
评论(0)