关于从视频中提取特定帧图片的常见方法及问题
=====================

#### 问题现象

在获取视频特定帧图片时，有以下三种不同的场景：

*   访问本地视频（沙箱内），获取视频特定帧图片；
*   访问网络视频，获取视频特定帧图片；
*   访问手机系统相册，获取视频特定帧图片。

同时，在这三种常见获取视频的场景里，也会遇到一些可能发生的问题：

*   特定帧获取异常；
*   特定帧图像模糊。

#### 背景知识

获取视频特定帧根据场景需求可以使用不同的方法实现：

*   AVImageGenerator.[fetchFrameByTime](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avimagegenerator#fetchframebytime12-1)：异步方式获取视频缩略图。
*   AVMetadataExtractor.[fetchFrameByTime](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avmetadataextractor#fetchframebytime20)：异步获取视频缩略图，可直接获取网络视频的缩略图。
*   PhotoAsset.[getThumbnail](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-photoaccesshelper-photoasset#getthumbnail)：获取文件的缩略图，传入缩略图尺寸，使用promise方式返回异步结果。

| 
获取方式

 | 

使用场景

 | 

目标场景

 |
| :-- | :-- | :-- |
| 

AVImageGenerator.fetchFrameByTime

 | 

视频编辑、逐帧分析、自定义时间点截图

 | 

动态解码视频帧生成缩略图（支持任意时间点帧提取）

 |
| 

AVImageGenerator.fetchFrameByTime

 | 

获取网络视频任意时间视频帧，可用于视频播放预览

 | 

动态解码视频帧生成缩略图（支持获取网络视频任意时间视频帧）

 |
| 

PhotoAsset.getThumbnail

 | 

相册浏览、快速预览

 | 

直接获取相册中图片/视频的预生成缩略图

 |

#### 解决方案

**方案一：访问本地视频（沙箱内），获取视频特定帧图片。**

*   **业务场景：** 用户导入本地视频后，实时生成缩略图预览。
*   **实现步骤：**
    1.  读取视频文件：通过file.openSync或ResourceManager.getRawFd获取沙箱内视频文件的文件描述符（fd）；
    2.  创建AVImageGenerator：使用media.createAVImageGenerator()初始化图像生成器；
    3.  提取特定帧：调用fetchFrameByTime(0,AV\_IMAGE\_QUERY\_NEXT\_SYNC,{width,height})获取首帧的PixelMap对象。
*   **具体代码：**
    
    import { image } from '@kit.ImageKit';
    import { media } from '@kit.MediaKit';
    import { BusinessError } from '@ohos.base';
    import { fileIo as fs } from '@kit.CoreFileKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    
    @Entry
    @Component
    struct frameExtraction {
      _// pixelMap对象声明，用于图片显示_
      @State pixelMap: image.PixelMap | string | undefined = undefined;
    
      build() {
        Column() {
          Button() {
            Text('select').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
          }.margin({ top: 20 }).width(100).height(40).onClick(() => this.showPicker());
    
    
          Image(this.pixelMap).width(300).height(300).margin({ top: 20 });
        }.width('100%').height('100%');
      }
    
      showPicker() {
        _// 创建图库选择器对象实例_
        const photoViewPicker =
          new photoAccessHelper.PhotoViewPicker();
        _// 图库选项配置，类型与数量_
        let selectOptions = new photoAccessHelper.PhotoSelectOptions();
        selectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO\_TYPE;
        selectOptions.maxSelectNumber = 1;
        _// 调用select()接口拉起图库界面进行文件选择,文件选择成功后，返回PhotoSelectResult结果集_
        photoViewPicker.select(selectOptions)
          .then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
            _// 用一个全局变量存储返回的uri_
            if (photoSelectResult.photoUris?.\[0\]) {
              this.testFetchFrameByTime(photoSelectResult.photoUris\[0\]);
            }
          }).catch((err: BusinessError) => {
          console.error(\`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}\`);
        });
      }
    
      _// 获取缩略图_
      async testFetchFrameByTime(filePath: string) {
        let fileDescriptor: number | undefined;
    
        try {
          let fileDescriptor = fs.openSync(filePath, fs.OpenMode.READ\_ONLY);
    
          _// 创建AVImageGenerator_
          let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
          let avFileDescriptor: media.AVFileDescriptor = { fd: fileDescriptor.fd };
    
          avImageGenerator.fdSrc = avFileDescriptor;
    
          _// 初始化入参_
          let timeUs = 0; _// 需要获取的缩略图在视频中的时间点_
          let queryOption = media.AVImageQueryOptions.AV\_IMAGE\_QUERY\_NEXT\_SYNC; _// AV\_IMAGE\_QUERY\_NEXT\_SYNC表示选取传入时间点或之后的关键帧_
          _// 输出缩略图的格式参数_
          let param: media.PixelMapParams = {
            width: 300, _// 输出的缩略图宽度_
            height: 300 _// 输出的缩略图高度_
          };
    
          _// 获取缩略图_
          this.pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
    
          _// 释放AVImageGenerator资源_
          await avImageGenerator.release();
    
          console.info(\`release success.\`);
    
        } catch (e) {
          console.error('Error in fetchFrameByTime:', e);
        } finally {
          if (fileDescriptor !== undefined) {
            try {
              fs.closeSync(fileDescriptor);
              console.info('File descriptor closed.');
            } catch (closeError) {
              console.error('Failed to close file descriptor:', closeError);
            }
          }
        }
      }
    }
    
*   **效果图：**
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/fb/v3/PHnHNWiiQF6A89KrXdbxkA/zh-cn_image_0000002628392774.png?HW-CC-KV=V1&HW-CC-Date=20260709T134252Z&HW-CC-Expire=86400&HW-CC-Sign=0092597259A1E058869C7E31D5DD551AB00E4802C462D2205038677F1AD2F899 "点击放大")
    

*   **可能会遇到的问题：** 成功提取音视频文件首帧图像后，无法对原始音视频文件执行压缩操作。
    *   **问题根因：** 应用默认只能访问自身沙箱目录，其他目录的文件无法直接操作。
    *   **解决方案：** 若音视频文件存储在非沙箱路径，应用无法直接读取或修改，需通过fs接口将文件拷贝至沙箱目录后才能处理。实测：7.76M左右原视频，COMPRESS\_QUALITY\_LOW下为1.9M左右。
        
        安装三方库：ohpm install @ohos/videocompressor
        
        import { common } from '@kit.AbilityKit';
        import { fileIo as fs } from '@kit.CoreFileKit';
        import { promptAction } from '@kit.ArkUI';
        import { BusinessError } from '@ohos.base';
        import { VideoCompressor, CompressQuality, CompressorResponseCode } from '@ohos/videocompressor';
        
        @Entry
        @Component
        struct CompressVideo {
          private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
        
          async writeFile(destPath: string) {
            let srcFd: number | null = null;
            let file: fs.File | null = null;
        
            try {
              let context = this.getUIContext().getHostContext() as Context;
        
              let srcFileDescriptor = context.resourceManager.getRawFdSync('test.mp4'); _// 获取resources/rawfile目录下开发者根据实际情况替换_
              if (!srcFileDescriptor) {
                console.error('资源文件 test.mp4 不存在或无法读取');
                return;
              }
              srcFd = srcFileDescriptor.fd;
        
              file = fs.openSync(destPath, fs.OpenMode.CREATE | fs.OpenMode.READ\_WRITE);
        
              let bufsize = 4096;
              const buf = new ArrayBuffer(bufsize);
        
              let off = 0;
              let readedLen = 0;
        
              while (true) {
                const len = fs.readSync(srcFd, buf, {
                  offset: srcFileDescriptor.offset + off,
                  length: bufsize
                });
        
                if (len === 0) {
                  break;
                }
        
                readedLen += len;
                fs.writeSync(file.fd, buf, { offset: off, length: len });
                off += len;
        
                _// 动态调整buffer大小_
                const remaining = srcFileDescriptor.length - readedLen;
                if (remaining < bufsize) {
                  bufsize = remaining;
                }
              }
        
              promptAction.openToast({
                message: '写入沙盒成功: ' + destPath,
                duration: 2000
              });
        
              let videoCompressor = new VideoCompressor();
              videoCompressor.compressVideo(context, destPath, CompressQuality.COMPRESS\_QUALITY\_LOW)
                .then((data) => {
                  if (data.code === CompressorResponseCode.SUCCESS) {
                    console.info(\`压缩成功，输出路径: ${data.outputPath}\`);
                  } else {
                    console.error(\`压缩失败: ${data.message}\`);
                  }
                })
                .catch((err: BusinessError) => {
                  console.error(\`压缩异常: ${err.message}\`);
                });
        
            } catch (err) {
              console.error('写入文件时发生错误:', err);
              promptAction.openToast({
                message: '写入失败，请检查权限或路径',
                duration: 2000
              });
            } finally {
              if (srcFd !== null) {
                try {
                  fs.closeSync(srcFd);
                } catch (e) {
                  console.warn('关闭源文件流失败:', e);
                }
              }
              if (file !== null) {
                try {
                  fs.closeSync(file);
                } catch (e) {
                  console.warn('关闭目标文件流失败:', e);
                }
              }
            }
          }
        
          build() {
            Column() {
              Button() {
                Text('重写').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
              }.margin({ top: 20 }).width(100).height(40).onClick(() => {
                let path = this.context.getApplicationContext().cacheDir + '001.mp4';
                this.writeFile(path);
              });
            }.width('100%').height('100%');
          }
        }
        

*   **效果图：**
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/ea/v3/Ij-s8FQlTd6LpyzQBBgUKQ/zh-cn_image_0000002658792045.png?HW-CC-KV=V1&HW-CC-Date=20260709T134252Z&HW-CC-Expire=86400&HW-CC-Sign=0B3D8F0B85FE449D2AFCDA48A83AB689A9D4F3E2C6E3DB2A120DFAE30B1D35CD "点击放大")![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/d1/v3/y97HAF8_Q5ya3AccUPwmNw/zh-cn_image_0000002628552666.png?HW-CC-KV=V1&HW-CC-Date=20260709T134252Z&HW-CC-Expire=86400&HW-CC-Sign=D2BCC36C98589C005422DBFD73C739FA7DD53C2EE07C31736511BE2099F5F13C "点击放大")
    

**方案二：下载网络视频到沙箱，然后获取视频特定帧图片。**

*   **业务场景一：** 在6.0之前，无法直接获取网络视频的特定时间帧。需要下载视频到本地后再提取视频帧。
*   **实现步骤：**
    1.  需添加网络权限：[ohos.permission.INTERNET](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/permissions-for-all#ohospermissioninternet)。
    2.  下载视频到沙箱：使用request.agent.create下载网络视频到沙箱临时目录。
    3.  读取本地缓存文件：下载完成后，通过文件路径调用本地提取方案。
*   **具体代码：**
    
    import { image } from '@kit.ImageKit';
    import { media } from '@kit.MediaKit';
    import { request } from '@kit.BasicServicesKit';
    import type { common } from '@kit.AbilityKit';
    import type { BusinessError } from '@ohos.base';
    import { fileIo as fs } from '@kit.CoreFileKit';
    
    _// 下载并提取第一帧_
    async function downloadAndExtractFrame(context: Context, url: string, tempFilePath: string): Promise<void> {
      _// 下载任务配置_
      let config: request.agent.Config = {
        action: request.agent.Action.DOWNLOAD,
        url: url,
        mode: request.agent.Mode.BACKGROUND,
        saveas: tempFilePath,
        overwrite: true,
        network: request.NETWORK\_WIFI, _// 仅限WiFi下载_
        method: 'GET'
      };
    
      try {
        const task = await new Promise<request.agent.Task>((resolve, reject) => {
          request.agent.create(context, config, (err: BusinessError, task: request.agent.Task) => {
            if (err) {
              reject(err);
              return;
            }
            console.info(\`下载任务创建成功: ${JSON.stringify(task.config)}\`);
            task.start((err: BusinessError) => {
              if (err) {
                console.error(\`Failed to start the download task, Code: ${err.code}, message: ${err.message}\`);
                return;
              }
              console.info(\`Succeeded in starting a download task.\`);
            });
            resolve(task);
          });
        });
    
        await new Promise<void>((resolve, reject) => {
          task.on('completed', () => {
            console.info('下载完成，开始提取第一帧');
            request.agent.remove(task.tid);
            resolve();
          });
          task.on('failed', (error) => {
            console.error('下载失败:', JSON.stringify(error));
            reject(error);
          });
        });
      } catch (err) {
        console.error(\`下载失败: ${JSON.stringify(err)}\`);
      }
    }
    
    _// 提取视频第一帧_
    async function getLocalVideoFirstFrame(filePath: string): Promise<image.PixelMap | null> {
      let file: fs.File | undefined = undefined;
      try {
        let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
        file = fs.openSync(filePath, fs.OpenMode.READ\_ONLY);
        let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
        avImageGenerator.fdSrc = avFileDescriptor;
    
        let timeUs = 0;
        let queryOption = media.AVImageQueryOptions.AV\_IMAGE\_QUERY\_NEXT\_SYNC;
        let param: media.PixelMapParams = {
          width: 1920,
          height: 1080
        };
    
        let pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
        avImageGenerator.release();
        console.info('第一帧提取成功');
        return pixelMap;
      } catch (err) {
        console.error('提取第一帧失败:', err);
        return null;
      } finally {
        if (file !== undefined) {
          try {
            fs.closeSync(file);
            console.info('File descriptor closed.');
          } catch (closeError) {
            console.error('Failed to close file descriptor:', closeError);
          }
        }
      }
    }
    
    @Entry
    @Component
    struct networkFrame {
      @State pixelMap: image.PixelMap | null = null;
      ratio: number = 16 / 9;
      private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    
      build() {
        Column() {
          Button('下载')
            .onClick(async () => {
              const url = 'https://xxx'; _// 开发者根据实际替换_
              const cacheDir = this.context.filesDir;
              const tempFilePath = \`${cacheDir}/temp\_video.mp4\`; _// 开发者根据实际替换下载保存路径_
              try {
                _// 1.下载视频_
                await downloadAndExtractFrame(this.context, url, tempFilePath);
    
                _// 2.提取第一帧_
                const pixelMap = await getLocalVideoFirstFrame(tempFilePath);
                if (pixelMap) {
                  this.pixelMap = pixelMap;
                  console.info('第一帧提取成功，已显示');
                } else {
                  console.warn('提取第一帧失败，返回 null');
                }
              } catch (err) {
                console.error('下载或提取失败:', err);
              }
            });
          Image(this.pixelMap)
            .width('100%')
            .aspectRatio(this.ratio)
            .margin({ top: 20 });
        }
        .height('100%')
        .width('100%');
      }
    }
    
*   **效果图：**
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/b8/v3/Gs0hP5inT561S9B5zX6xBw/zh-cn_image_0000002658911987.png?HW-CC-KV=V1&HW-CC-Date=20260709T134252Z&HW-CC-Expire=86400&HW-CC-Sign=B862E8B7E12E9792B9A76363CCEF28653C5A2EA86A3EC7794173E86457614783 "点击放大")
    
*   **业务场景二：** 对于大数量的大视频文件，如何展示所有视频的封面图。
*   **解决方案**：缓存视频资源过多容易因为内存占用过多导致应用崩溃。API 17起，用户可以通过[setMediaSource](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avplayer#setmediasource12)预下载流媒体资源，并通过设置[PlaybackStrategy](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-i#playbackstrategy12)中的showFirstFrameOnPrepare实现封面图展示。当参数设置为true时，Prepare之后便显示视频起播首帧，参考如下。
    
    async function setupPlayer() {
      let headers: Record<string, string> = { 'User-Agent': 'User-Agent-Value' };
      let mediaSource: media.MediaSource = media.createMediaSourceWithUrl('http://xxx', headers);
      let uuid: number = 1;
      let requests: HashMap<number, media.MediaSourceLoadingRequest> = new HashMap();
    
      let sourceOpenCallback: media.SourceOpenCallback = (request: media.MediaSourceLoadingRequest) => {
        console.info(\`Opening resource: ${request.url}\`);
        _// 成功打开资源，返回唯一的句柄, 保证uuid和request对应_
        uuid += 1;
        requests.set(uuid, request);
        return uuid;
      };
    
      let sourceReadCallback: media.SourceReadCallback =
        (uuid: number, requestedOffset: number, requestedLength: number) => {
          console.info(\`Reading resource with handle ${uuid}, offset: ${requestedOffset}, length: ${requestedLength}\`);
          _// 判断uuid是否合法、存储read请求，不要在read请求阻塞去推送数据和头信息_
        };
    
      let sourceCloseCallback: media.SourceCloseCallback = (uuid: number) => {
        console.info(\`Closing resource with handle ${uuid}\`);
        _// 清除当前uuid相关资源_
        requests.remove(uuid);
      };
    
      _// 应用按需实现_
      let mediaSourceLoader: media.MediaSourceLoader = {
        open: sourceOpenCallback,
        read: sourceReadCallback,
        close: sourceCloseCallback
      };
    
      mediaSource.setMediaResourceLoaderDelegate(mediaSourceLoader);
      let playStrategy: media.PlaybackStrategy = {
        preferredBufferDuration: 20,
        showFirstFrameOnPrepare: true
      };
    
      let player = await media.createAVPlayer();
      player.setMediaSource(mediaSource, playStrategy);
    }
    

**方案三：通过网络视频Url，直接获取视频特定帧图片。**

*   **业务场景一：** 在6.0之后，可以通过AVMetadataExtractor.[fetchFrameByTime](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avmetadataextractor#fetchframebytime20)接口，直接获取网络视频特定时间的视频帧。
*   **实现步骤：**
    1.  通过[createAVMetadataExtractor](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-f#mediacreateavmetadataextractor11)接口，创建AVMetadataExtractor。
    2.  通过[setUrlSource](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avmetadataextractor#seturlsource20)接口，设置网络视频Url。
    3.  通过[fetchFrameByTime](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avmetadataextractor#fetchframebytime20)接口，获取网络视频特定帧的速率图。
*   **具体代码：**
    
    import { media } from '@kit.MediaKit';
    import image from '@ohos.multimedia.image';
    
    const VIDEO\_URL = 'xxxx'; _// 网络视频url_
    
    @Entry
    @Component
    struct AVMetadataExtractorDemo {
      @State pixelMap?: image.PixelMap = undefined; _// 用于显示缩略图_
    
      _/\*\*_
       _\* 获取网络视频指定时间的缩略图（仅支持http/https，不支持hls/dash）_
       _\* @param url 网络视频Url_
       _\* @param timeUs 指定需要获取视频帧的时间_
       _\*/_
      async fetchFrameByTime(url: string, timeUs: number) {
        let oldPixelMap: image.PixelMap | undefined;
        let avMetadataExtractor: media.AVMetadataExtractor | undefined = undefined;
        try {
          _// 创建AVMetadataExtractor_
          avMetadataExtractor = await media.createAVMetadataExtractor();
          _// 设置网络视频url_
          avMetadataExtractor.setUrlSource(url);
          _// 获取指定时间视频帧_
          let param: media.PixelMapParams = { width: 300, height: 300 };
          let queryOptions: media.AVImageQueryOptions = media.AVImageQueryOptions.AV\_IMAGE\_QUERY\_PREVIOUS\_SYNC;
          let newPixelMap = await avMetadataExtractor.fetchFrameByTime(timeUs, queryOptions, param);
          _// 显示图片_
          oldPixelMap = this.pixelMap;
          this.pixelMap = newPixelMap;
        } catch (error) {
          let err = error as BusinessError;
          console.error(\`Failed to fetchFrameByTime: Cause: ${err.code}, ${err.message}\`);
        } finally {
          if (avMetadataExtractor) {
            await avMetadataExtractor.release();
          }
          if (oldPixelMap) {
            await oldPixelMap.release();
          }
        }
      }
    
      build() {
        Column() {
          Button() {
            Text('select').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White);
          }
          .width(100)
          .height(40)
          .margin({ top: 20 })
          .onClick(() => {
            const timeUs = 0;
            this.fetchFrameByTime(VIDEO\_URL, timeUs);
          });
    
          Image(this.pixelMap).width(300).height(300).margin({ top: 20 });
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center);
      }
    }
    

**方案四：访问手机系统相册，获取视频特定帧图片**。

*   **适用场景：** 用户从相册选择视频后，快速显示特定帧作为封面。
*   **实现步骤：**
    1.  查询视频URI：通过AVStorage查询相册视频的URI。
    2.  提取首帧：使用AVImageGenerator从URI提取特定帧。
*   **具体代码：**
    
    import { media } from '@kit.MediaKit';
    import { image } from '@kit.ImageKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    import { fileIo as fs } from '@kit.CoreFileKit';
    
    @Entry
    @Component
    struct AVImageGenera {
      @State uris: Array<string> = \[\];
      @State duration: number = 0;
      @State times: number = 0;
      _// pixelMap对象声明，用于图片显示_
       pixelMap: image.PixelMap | undefined = undefined;
      _// pixelMap对象数组，用于保存取到的帧图_
      @State pixelMapArr: image.PixelMap\[\] = \[\];
    
      build() {
        Row() {
          Column({ space: 10 }) {
            Button('打开相册,选择视频')
              .onClick(() => {
                _// 缩略图数组清空_
                this.pixelMapArr = \[\];
                _// 使用picker打开图库，选择一个视频，保存其uri_
                const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
                photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO\_TYPE; _// 过滤选择媒体文件类型为IMAGE_
                photoSelectOptions.maxSelectNumber = 1; _// 选择媒体文件的最大数目_
                const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
                photoViewPicker.select(photoSelectOptions)
                  .then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
                    this.uris = photoSelectResult.photoUris;
                    _// 获取缩略图_
                    this.testFetchFrameByTime();
                  });
              });
            Image(this.pixelMapArr\[0\])
              .width(300);
          }
          .width('100%');
        }
        .height('100%');
      }
    
      _// 获取视频指定时间的缩略图，并通过Image控件显示在屏幕上_
      async testFetchFrameByTime() {
        _// 获取视频时长，单位是毫秒_
        let avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
        avMetadataExtractor.fdSrc = await fs.open(this.uris\[0\]);
        avMetadataExtractor.fetchMetadata().then((metadata: media.AVMetadata) => {
          console.info(\`Succeeded in fetching Metadata, genre: ${metadata.duration}\`);
          this.duration = Number(metadata.duration);
          _// 若需要间隔2秒获取一次，则用视频时长计算需要获取多少次(向下取整,加上0秒获取的第一次)_
          this.times = Math.floor(this.duration / 2000) + 1;
          this.createImageGeneratorAndFetchFrames();
        });
      }
    
      async createImageGeneratorAndFetchFrames() {
        _// 以下为获取缩略图_
        _// 创建AVImageGenerator对象_
        let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
        _/__/ 设置fdSrc_
        avImageGenerator.fdSrc = await fs.open(this.uris\[0\]);
        _// 初始化入参_
        let timeUs = 0;
        let queryOption = media.AVImageQueryOptions.AV\_IMAGE\_QUERY\_NEXT\_SYNC;
        let param: media.PixelMapParams = {
          width: 300,
          height: 300
        };
        _// 获取缩略图，参数单位是微秒（promise模式）_
        _// 这里for循环控制获取多少次缩略图，timeUs控制每次间隔多少微秒(2秒=2000000微秒)_
        for (let i = 1; i <= this.times; i++) {
          this.pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
          this.pixelMapArr.push(this.pixelMap);
          console.log(JSON.stringify(this.pixelMapArr));
          timeUs = timeUs + 2000000;
        }
        _// 释放资源（promise模式）_
        avImageGenerator.release();
      };
    }
    
*   **效果图：**
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/44/v3/GWq9T-jETKSsRgGLzz4t0w/zh-cn_image_0000002628392778.png?HW-CC-KV=V1&HW-CC-Date=20260709T134252Z&HW-CC-Expire=86400&HW-CC-Sign=FDD4DA5451123D3680C79F69F8238BAD1397B5ADD4F675D274BF0B7C695D9566 "点击放大")
    
*   **可能会遇到的问题：** 无法从系统相册中读取视频文件的帧序列数据。
    *   **问题根因：**
        1.  **权限问题：**
            
            当前读取媒体库图片或视频有两种方案：
            
            1.  推荐方案（无需申请权限）：使用[Picker](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/photoaccesshelper-photoviewpicker)读取媒体库的图片与视频；使用保存控件保存媒体库的图片与视频。
            2.  受限使用方案：申请受限权限[ohos.permission.READ\_IMAGEVIDEO](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/restricted-permissions#ohospermissionread_imagevideo)或[ohos.permission.WRITE\_IMAGEVIDEO](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/restricted-permissions#ohospermissionwrite_imagevideo)读取媒体库的图片与视频。
            
        2.  **文件路径未适配沙箱机制：**
            
            相册文件默认存储在系统沙箱外（如content://media/external/video/media），应用需通过[fs(文件管理)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-file-fs)接口将文件复制到沙箱目录后才能访问。
            
        3.  **视频解码器配置错误：**
            
            硬件编解码限制：HarmonyOS优先使用硬件解码（如H.264/HEVC），但部分设备可能不支持特定格式。若视频编码格式（如AV1）不被硬件支持，需切换至软件解码。
            
        4.  **文件状态异常：**
            
            文件被占用：若视频文件正在被其他进程（如相册应用）播放或编辑，当前应用无法读取。
            
            文件损坏：视频文件头信息缺失或损坏，导致解码器无法解析。
            

#### 常见FAQ

Q：提取的特定帧图像分辨率显著低于源视频实际分辨率，产生模糊现象。

A：[PixelMapParams](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-i#pixelmapparams12)的格式参数设置错误，导致获取到的首帧图像的分辨率与实际的图像分辨率不同，从而导致首帧图像的模糊。需将特定帧图像的分辨率与实际的图像分辨率设置相同（或比例增大），确保[PixelMapParams](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-i#pixelmapparams12)与视频的实际帧率和分辨率匹配。

核心代码如下：

_// 以下为视频的原始分辨率_
let param: media.PixelMapParams = {
  width:1280,
  height:720
}

输出结果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/95/v3/GC9nljbQSyidZGeDmqYZBg/zh-cn_image_0000002658792049.png?HW-CC-KV=V1&HW-CC-Date=20260709T134252Z&HW-CC-Expire=86400&HW-CC-Sign=64C17CFCB7A91679A9614AB0B8043E98BAB1909F5A447D96DA6CB01A5DB45D36 "点击放大")

Q：为什么相册选择视频后,获取到视频缩略图并转成PixelMap,封装成对象msg：{thumbnail: pixelMap},传到另一个组件@Prop msg无法展示?

A：@Prop不支持PixelMap类型，具体可参考@Prop组件的限制条件。

Q：为什么getThumbnail获取到的PixelMap，用JSON.stringify打印出来是空对象？

A：PixelMap对象不是标准的JS对象，而是HarmonyOS提供的图像对象，所以无法使用JSON.stringify打印，可以通过Image组件验证是否可以正确显示图片。

Q：获取缩略图的PixelMap后，将PixelMap转为图片路径时，会报PackedSize outside size想问一下怎么设置这个参数。

A：bufferSize是默认为25M，可以使用[packToFile()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagepacker#packtofile11)将PixelMap编码后直接打包进文件，使用packToFile不受bufferSize参数影响，这个报错可能是由于PixelMap超过图片格式分辨率导致打成文件失败，过大的PixelMap建议修改PixelMap大小并且修改格式为png或分辨率更大的格式，但还是建议内存空间（bufferSize参数）的设置要大于图片大小。

Q：为什么使用AVImageGenerator获取视频帧图像出现不同时间点获取的图像是相同的，想获取时间点的图像没有获取到，获取的图像均存在连续多张相同情况？

A：获取方式为AV\_IMAGE\_QUERY\_NEXT\_SYNC、AV\_IMAGE\_QUERY\_PREVIOUS\_SYNC、AV\_IMAGE\_QUERY\_CLOSEST\_SYNC时都是选取传入时间点附近的关键帧返回，如果传入时间点对应的视频帧不是关键帧，会根据获取方式的不同选择传入时间点之前、之后或最近的关键帧返回。所以造成未匹配的现象，若想要尽量获取任意时间点的帧图片，可以尝试通过media.AVImageQueryOptions.AV\_IMAGE\_QUERY\_CLOSEST来获取对应时间点的视频帧，获取的是离传入时间点最近的帧，该帧不一定是关键帧。

#### 总结

在HarmonyOS开发中，不同场景下获取视频特定帧的业务需求存在显著差异。以下是四个典型场景的具体业务案例对比：

| 
维度

 | 

沙箱本地视频

 | 

网络视频

 | 

系统相册

 |
| :-- | :-- | :-- | :-- |
| 

数据来源

 | 

应用私有目录

 | 

远程服务器

 | 

用户相册

 |
| 

适用场景

 | 

视频编辑、本地播放

 | 

短视频、直播预加载

 | 

相册选择、媒体库管理

 |