HarmonyOS开发视频缩略图

举报
Jack20 发表于 2026/06/20 21:20:32 2026/06/20
【摘要】 HarmonyOS开发中的视频缩略图:@ohos.multimedia.media、缩略图提取、时间点截图、缩略图缓存、批量提取核心要点:掌握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提供的视频帧图像提取器,其核心工作流程:

  1. 创建实例:通过media.createAVImageGenerator()创建
  2. 设置数据源:配置fdSrc(文件描述符)或url(文件路径)
  3. 请求帧数据:调用fetchFrameByTime()指定时间点获取图像
  4. 释放资源:使用完毕后调用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 性能优化建议

  1. 预提取:在视频列表加载时,提前提取前几页的缩略图
  2. 降级策略:提取失败时使用视频第一帧或默认占位图
  3. 并发控制:使用任务队列控制同时提取的数量
  4. 分辨率适配:根据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缩略图
      系统级缓存

关键要点回顾

  1. AVImageGenerator是核心:灵活可控,支持指定时间点和分辨率提取,是缩略图提取的首选方案
  2. 缓存是必须的:没有缓存的缩略图系统就是性能灾难,内存+磁盘两级缓存缺一不可
  3. PixelMap生命周期:这是最容易踩的坑,忘记释放PixelMap会导致内存泄漏,必须严格管理
  4. 批量提取要控并发:同时提取太多帧会导致CPU/GPU过载,建议并发数不超过3
  5. HarmonyOS 6大幅增强:批量接口、快速模式、系统级缓存,开发效率和使用体验都有质的提升

下一篇我们将深入字幕渲染技术,看看如何解析SRT/ASS字幕并实现精确同步渲染。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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