HarmonyOS 鸿蒙Next开发中遇到的获取视频帧图像连续重复问题

HarmonyOS 鸿蒙Next开发中遇到的获取视频帧图像连续重复问题 1、开发工具及环境:DevEco Studio 5.0.0 Release Build #DS-233.14475.28.36.503910 构建版本:5.0.3.910, built on November 1, 2024 Runtime version: 17.0.12+1-b1087.25 amd64 VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. Windows 10.0 HarmonyOS 5.0.0 Release SDK,基于OpenHarmony SDK Ohos_sdk_public 5.0.0.71 (API Version 12 Release)

2、现在的问题:开发获取一个视频连续帧图像时,在帧率从10FPS到240FPS等不同选项中,获取的图像均存在连续多张相同情况,而且视频中很多帧图像没有获取到,即使240FPS帧率获取,12秒的视频,最多出现连续70多张一样的图像,但又有很多视频中的图像没有获取到。

3、使用的相关代码:使用的是官方AVImageGenerator.fetchFrameByTime异步获取方式。参照的是关于从视频中提取特定帧图片的常见方法及问题-行业常见问题-实用工具类行业实践-场景化知识 - 华为HarmonyOS开发者https://developer.huawei.com/consumer/cn/doc/architecture-guides/tools-v1_2-ts_122-0000002367622350#section935082815916)这里的案例代码。对于视频帧的获取方式

media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC ,

media.AVImageQueryOptions.AV_IMAGE_QUERY_PREVIOUS_SYNC,

 media.AVImageQueryOptions.AV_IMAGE_QUERY_CLOSEST_SYNC,

都进行了尝试,结果都是一样的,错过很多帧,又会出现很多连续相同帧图像。

我实现的代码如下:

import media from '@ohos.multimedia.media';
import image from '@ohos.multimedia.image';
import { BusinessError } from '@ohos.base';
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import photoAccessHelper from '@ohos.file.photoAccessHelper';

// 查询选项接口
interface QueryOption {
  name: string;
  option: media.AVImageQueryOptions;
}

@Entry
@Component
export struct FrameTestPage {
  // pixelMap对象声明,用于图片显示
  @State pixelMaps: image.PixelMap[] = [];
  @State currentFrameIndex: number = 0;
  @State isExtracting: boolean = false;
  @State selectedVideoPath: string = '';
  @State frameComparisonResults: string[] = [];

  build() {
    Column() {
      // 标题
      Text('官方代码帧提取测试')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFD700')
        .margin({ bottom: 20 })

      // 选择视频按钮
      Button('选择视频文件')
        .width(200)
        .height(50)
        .fontSize(16)
        .backgroundColor('#FF8C00')
        .fontColor('#FFFFFF')
        .borderRadius(25)
        .onClick(() => this.showPicker())
        .margin({ bottom: 20 })

      // 提取多帧按钮
      Button('提取多帧测试')
        .width(200)
        .height(50)
        .fontSize(16)
        .backgroundColor('#FF8C00')
        .fontColor('#FFFFFF')
        .borderRadius(25)
        .onClick(() => this.testMultipleFrames())
        .enabled(this.selectedVideoPath.length > 0)
        .margin({ bottom: 10 })

      // 测试不同查询选项
      Button('测试不同查询选项')
        .width(200)
        .height(50)
        .fontSize(16)
        .backgroundColor('#32CD32')
        .fontColor('#FFFFFF')
        .borderRadius(25)
        .onClick(() => this.testDifferentQueryOptions())
        .enabled(this.selectedVideoPath.length > 0)
        .margin({ bottom: 20 })

      // 文件路径显示
      if (this.selectedVideoPath) {
        Text(`已选择: ${this.selectedVideoPath.split('/').pop()}`)
          .fontSize(12)
          .fontColor('#CCCCCC')
          .margin({ bottom: 10 })
      }

      // 提取状态
      if (this.isExtracting) {
        Column() {
          Row() {
            LoadingProgress()
              .width(20)
              .height(20)
              .color('#FFD700')
              .margin({ right: 10 })
            
            Text('正在提取帧...')
              .fontSize(14)
              .fontColor('#FFD700')
          }
          
          Text('间隔10毫秒,提取50帧')
            .fontSize(12)
            .fontColor('#CCCCCC')
            .margin({ top: 5 })
        }
        .margin({ bottom: 20 })
      }

      // 帧显示区域
      if (this.pixelMaps.length > 0) {
        Text(`已提取 ${this.pixelMaps.length} 帧`)
          .fontSize(16)
          .fontColor('#FFFFFF')
          .margin({ bottom: 10 })

        // 当前帧显示
        Image(this.pixelMaps[this.currentFrameIndex])
          .width(300)
          .height(200)
          .objectFit(ImageFit.Contain)
          .border({ width: 2, color: '#FFD700' })
          .margin({ bottom: 20 })

        // 帧导航
        Row() {
          Button('上一帧')
            .width(80)
            .height(40)
            .enabled(this.currentFrameIndex > 0)
            .onClick(() => {
              if (this.currentFrameIndex > 0) {
                this.currentFrameIndex--;
              }
            })

          Text(`第 ${this.currentFrameIndex + 1} 帧`)
            .fontSize(14)
            .fontColor('#FFFFFF')
            .margin({ left: 20, right: 20 })

          Button('下一帧')
            .width(80)
            .height(40)
            .enabled(this.currentFrameIndex < this.pixelMaps.length - 1)
            .onClick(() => {
              if (this.currentFrameIndex < this.pixelMaps.length - 1) {
                this.currentFrameIndex++;
              }
            })
        }
        .margin({ bottom: 20 })

        // 帧缩略图列表
        Scroll() {
          Row() {
            ForEach(this.pixelMaps, (pixelMap: image.PixelMap, index: number) => {
              Image(pixelMap)
                .width(40)
                .height(30)
                .border({
                  width: this.currentFrameIndex === index ? 2 : 1,
                  color: this.currentFrameIndex === index ? '#FFD700' : '#666666'
                })
                .onClick(() => {
                  this.currentFrameIndex = index;
                })
                .margin({ right: 4 })
            })
          }
          .padding({ left: 20, right: 20 })
        }
        .width('100%')
        .height(50)
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Auto)

        // 帧比较结果
        if (this.frameComparisonResults.length > 0) {
          Column() {
            Text('帧比较结果:')
              .fontSize(16)
              .fontColor('#FFD700')
              .margin({ bottom: 10 })

            ForEach(this.frameComparisonResults, (result: string, index: number) => {
              Text(result)
                .fontSize(12)
                .fontColor('#FFFFFF')
                .margin({ bottom: 2 })
            })
          }
          .width('100%')
          .margin({ top: 20 })
        }
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#000000')
  }

  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: picker.PhotoSelectResult) => {
        // 用一个全局变量存储返回的uri
        if (photoSelectResult.photoUris?.[0]) {
          this.selectedVideoPath = 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) {
    try {
      console.info('FrameTestPage: 开始测试官方代码');
      
      // 创建AVImageGenerator对象
      let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
      let 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: 300,
        height: 400,
      }
      
      // 获取缩略图(promise模式)
      const pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
      
      if (pixelMap) {
        this.pixelMaps = [pixelMap];
        this.currentFrameIndex = 0;
        console.info('FrameTestPage: 单帧提取成功');
      }
      
      // 释放资源(promise模式)
      avImageGenerator.release()
      console.info('FrameTestPage: release success.')
      fs.closeSync(file);
    } catch (error) {
      console.error('FrameTestPage: 单帧提取失败', error);
    }
  }

  // 测试多帧提取
  async testMultipleFrames() {
    try {
      console.info('FrameTestPage: 开始多帧测试');
      console.info('FrameTestPage: 使用文件路径', this.selectedVideoPath);
      
      if (!this.selectedVideoPath) {
        console.error('FrameTestPage: 未选择视频文件');
        return;
      }
      
      this.isExtracting = true;
      
      // 创建AVImageGenerator对象
      let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
      let file = fs.openSync(this.selectedVideoPath, 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: 300,
        height: 400,
      }
      
      const pixelMaps: image.PixelMap[] = [];
      
      // 提取50帧,每帧间隔10毫秒
      for (let i = 0; i < 50; i++) {
        console.info(`FrameTestPage: 提取第 ${i + 1} 帧,时间 ${timeUs} 微秒`);
        
        try {
          const pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
          if (pixelMap) {
            pixelMaps.push(pixelMap);
            console.info(`FrameTestPage: 第 ${i + 1} 帧提取成功`);
          } else {
            console.warn(`FrameTestPage: 第 ${i + 1} 帧为空`);
          }
        } catch (error) {
          console.error(`FrameTestPage: 第 ${i + 1} 帧提取失败`, error);
        }
        
        // 按照官方方式,累积时间戳(10毫秒=10000微秒)
        timeUs += 1000000;
      }
      
      this.pixelMaps = pixelMaps;
      this.currentFrameIndex = 0;
      this.isExtracting = false;
      
      console.info(`FrameTestPage: 多帧提取完成,共 ${pixelMaps.length} 帧`);
      
      // 释放资源
      avImageGenerator.release()
      fs.closeSync(file);
    } catch (error) {
      console.error('FrameTestPage: 多帧提取失败', error);
      this.isExtracting = false;
    }
  }

  // 测试不同查询选项
  async testDifferentQueryOptions() {
    try {
      console.info('FrameTestPage: 开始测试不同查询选项');
      
      if (!this.selectedVideoPath) {
        console.error('FrameTestPage: 未选择视频文件');
        return;
      }
      
      this.isExtracting = true;
      this.frameComparisonResults = [];
      
      // 创建AVImageGenerator对象
      let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
      let file = fs.openSync(this.selectedVideoPath, fs.OpenMode.READ_ONLY);
      let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
      avImageGenerator.fdSrc = avFileDescriptor;
      
      // 测试不同的查询选项
      const queryOptions: QueryOption[] = [
        { name: 'NEXT_SYNC', option: media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC },
        { name: 'PREV_SYNC', option: media.AVImageQueryOptions.AV_IMAGE_QUERY_PREVIOUS_SYNC },
        { name: 'CLOSEST_SYNC', option: media.AVImageQueryOptions.AV_IMAGE_QUERY_CLOSEST_SYNC }
      ];
      
      const pixelMaps: image.PixelMap[] = [];
      
      for (let queryIndex = 0; queryIndex < queryOptions.length; queryIndex++) {
        const queryOption = queryOptions[queryIndex];
        console.info(`FrameTestPage: 测试查询选项 ${queryOption.name}`);
        
        // 每个查询选项提取5帧
        for (let i = 0; i < 5; i++) {
          const timeUs = i * 1000000; // 每帧间隔1秒
          console.info(`FrameTestPage: ${queryOption.name} - 提取第 ${i + 1} 帧,时间 ${timeUs} 微秒`);
          
          try {
            const pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption.option, {
              width: 300,
              height: 400,
            });
            
            if (pixelMap) {
              pixelMaps.push(pixelMap);
              console.info(`FrameTestPage: ${queryOption.name} - 第 ${i + 1} 帧提取成功`);
            } else {
              console.warn(`FrameTestPage: ${queryOption.name} - 第 ${i + 1} 帧为空`);
            }
          } catch (error) {
            console.error(`FrameTestPage: ${queryOption.name} - 第 ${i + 1} 帧提取失败`, error);
          }
        }
      }
      
      this.pixelMaps = pixelMaps;
      this.currentFrameIndex = 0;
      this.isExtracting = false;
      
      // 分析帧差异
      this.analyzeFrameDifferences();
      
      console.info(`FrameTestPage: 不同查询选项测试完成,共 ${pixelMaps.length} 帧`);
      
      // 释放资源
      avImageGenerator.release()
      fs.closeSync(file);
    } catch (error) {
      console.error('FrameTestPage: 不同查询选项测试失败', error);
      this.isExtracting = false;
    }
  }

  // 分析帧差异
  analyzeFrameDifferences() {
    
  }
}

请各位大神百忙之中给予指导。不胜感激。谢谢。


更多关于HarmonyOS 鸿蒙Next开发中遇到的获取视频帧图像连续重复问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

在HarmonyOS Next开发中,获取视频帧图像时出现连续重复问题,通常是由于帧处理回调触发频率不当或缓冲区未及时刷新导致。需检查VideoPlayeronFrameUpdate回调实现,确保每次回调都处理新帧数据而非复用旧帧。同时验证Image组件或自定义绘制的刷新机制是否与视频帧率同步。若使用PixelMap处理帧数据,确认其生命周期管理正确,避免同一帧被多次引用。可通过调整帧采样间隔或使用去重逻辑解决。

更多关于HarmonyOS 鸿蒙Next开发中遇到的获取视频帧图像连续重复问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next开发中,使用AVImageGenerator.fetchFrameByTime获取视频连续帧时出现重复帧和漏帧问题,这通常与视频编码特性及API工作机制相关。

问题分析:

  1. 视频编码特性:大多数视频采用关键帧(I帧)和预测帧(P/B帧)的压缩方式。AVImageGenerator在非关键帧位置可能返回最近的关键帧,导致连续相同帧。

  2. 时间戳精度:虽然你设置了10ms间隔(10000微秒),但视频的实际帧间隔可能不匹配,特别是低帧率视频。

  3. 查询选项限制:三种查询选项(NEXT_SYNC、PREVIOUS_SYNC、CLOSEST_SYNC)在关键帧稀疏的视频中都会倾向于返回相同的关键帧。

优化建议:

  1. 获取视频元信息
// 在创建AVImageGenerator后获取视频时长和帧率
const duration = await avImageGenerator.getVideoDuration();
const frameRate = await avImageGenerator.getVideoFrameRate();
  1. 基于实际帧率计算间隔
// 根据实际帧率计算时间间隔
const frameInterval = 1000000 / frameRate; // 微秒
  1. 考虑使用seek模式:对于连续帧提取,创建AVPlayer进行精确seek可能更可靠。

  2. 验证视频源:测试不同编码格式的视频(H.264、H.265),观察问题是否与特定编码相关。

当前实现中时间戳累积方式(每帧增加1000000微秒)在高速帧率下可能导致跳过实际帧,建议基于视频实际帧率动态调整提取间隔。

回到顶部