HarmonyOS 鸿蒙Next中怎么获取视频特定帧图片

HarmonyOS 鸿蒙Next中怎么获取视频特定帧图片 如题,如何从视频中提取特定帧图片?

8 回复

背景知识:

AVImageGenerator.fetchFrameByTime:异步方式获取视频缩略图。

PhotoAsset.getThumbnail:获取文件的缩略图,传入缩略图尺寸,使用promise方式返回异步结果。

问题处理:

示例代码:

AVImageGenerator.fetchFrameByTime:

import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.FileIO';

// 获取沙箱文件描述符
async function getVideoFD(): Promise<number> {
  const sandboxPath = '应用沙箱路径/video.mp4';
  return fileIo.open(sandboxPath, fileIo.OpenMode.READ_ONLY);
}

async function fetchFrame() {
  const avImageGenerator = media.createAVImageGenerator();
  avImageGenerator.fdSrc = await getVideoFD(); // 设置文件描述符
  
  // 获取第3秒的帧(单位微秒)
  const pixelMap = await avImageGenerator.fetchFrameByTime(
    3000000, 
    media.AVImageQueryOptions.AV_IMAGE_QUERY_CLOSEST_SYNC,
    { width: 720, height: 480 }
  );
  
  // 显示到Image组件
  this.imageSrc = pixelMap;
  
  // 释放资源
  avImageGenerator.release();
}

PhotoAsset.getThumbnail:

import { photoAccessHelper } from '@kit.MediaLibraryKit';

async function getGalleryThumbnail() {
  const albums = await photoAccessHelper.getAlbums('VIDEO');
  const videoAlbum = albums;
  const assets = await videoAlbum.getAssets();
  
  const videoAsset = assets;
  const pixelMap = await videoAsset.getThumbnail({ width: 300, height: 200 });
  
  // 显示缩略图
  this.imageSrc = pixelMap;
}

可以参照:关于从视频中提取特定帧图片的常见方法及问题

更多关于HarmonyOS 鸿蒙Next中怎么获取视频特定帧图片的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


数据来源 核心方法 适用场景 关键注意点
沙箱本地视频 AVImageGenerator.fetchFrameByTime 本地编辑、播放预览 需获取文件 FD,释放资源
网络视频 下载 + 本地提取 短视频列表、直播封面 必须先下载到沙箱
系统相册视频 Picker+AVImageGenerator 相册选择、媒体管理 Picker选视频,免权限

参考: 关于从视频中提取特定帧图片的常见方法及问题-行业常见问题-实用工具类行业实践-场景化知识 - 华为HarmonyOS开发者

找HarmonyOS工作还需要会Flutter技术的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

可以参考 关于从视频中提取特定帧图片的常见方法及问题-行业常见问题-实用工具类行业实践

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

获取方式 使用场景 目标场景
AVImageGenerator.fetchFrameByTime 视频编辑、逐帧分析、自定义时间点截图。 动态解码视频帧生成缩略图(支持任意时间点帧提取)。
PhotoAsset.getThumbnail 相册浏览、快速预览。 直接获取相册中图片/视频的预生成缩略图。

原理:鸿蒙媒体框架的帧提取机制

鸿蒙系统通过媒体编解码框架图像处理管道实现视频帧提取。核心原理是利用AVCodec解码器读取视频流,通过时间戳定位到特定帧,然后使用Image组件将解码后的YUV/RGB数据转换为位图格式,最后通过image模块保存为图片文件。

报错原因:权限和编解码问题导致提取失败

  1. 权限不足:缺少ohos.permission.READ_MEDIAohos.permission.WRITE_MEDIA权限
  2. 编解码器不支持:视频格式不被系统解码器支持
  3. 内存不足:高分辨率视频帧占用大量内存
  4. 时间戳定位失败:seek操作无法精确到指定帧
  5. Surface配置错误:图像转换过程中的像素格式不匹配
  6. 文件路径无效:输出路径不存在或无写入权限

解决方案:使用鸿蒙原生媒体API

方法一:使用AVCodec解码器(推荐)

import media from '@ohos.multimedia.media';
import image from '@ohos.multimedia.image';
import fileio from '@ohos.fileio';

export class VideoFrameExtractor {
  private codec: media.Codec;
  private extractor: media.MediaExtractor;
  
  async extractFrame(videoPath: string, timestamp: number): Promise<image.Image> {
    try {
      // 创建媒体提取器
      this.extractor = await media.createMediaExtractor();
      await this.extractor.setDataSource(videoPath);
      
      // 获取视频轨道
      const trackCount = this.extractor.getTrackCount();
      let videoTrackIndex = -1;
      
      for (let i = 0; i < trackCount; i++) {
        const trackFormat = this.extractor.getTrackFormat(i);
        if (trackFormat.getString(media.MediaFormat.MIME).startsWith('video/')) {
          videoTrackIndex = i;
          break;
        }
      }
      
      if (videoTrackIndex === -1) {
        throw new Error('未找到视频轨道');
      }
      
      // 定位到指定时间戳
      this.extractor.selectTrack(videoTrackIndex);
      this.extractor.seekTo(timestamp, media.MediaExtractor.SeekMode.SEEK_TO_CLOSEST_SYNC);
      
      // 创建解码器
      const format = this.extractor.getTrackFormat(videoTrackIndex);
      this.codec = await this.createDecoder(format);
      
      // 提取帧
      return await this.decodeFrame();
    } catch (error) {
      console.error('帧提取失败:', error);
      throw error;
    }
  }
  
  private async createDecoder(format: media.MediaFormat): Promise<media.Codec> {
    const mime = format.getString(media.MediaFormat.MIME);
    const decoder = await media.createDecoder(mime);
    
    // 配置解码器
    decoder.configure({
      format: format,
      surface: null, // 不使用Surface,直接获取原始数据
      crypto: null,
      flags: 0
    });
    
    return decoder;
  }
  
  private async decodeFrame(): Promise<image.Image> {
    return new Promise((resolve, reject) => {
      this.codec.on('error', (err) => {
        reject(err);
      });
      
      this.codec.on('formatChanged', (format) => {
        console.info('解码格式变更:', format);
      });
      
      this.codec.on('inputAvailable', (inputBuffer) => {
        // 读取输入数据
        const sampleSize = this.extractor.readSampleData(inputBuffer, 0);
        if (sampleSize < 0) {
          this.codec.queueInputBuffer(inputBuffer, 0, 0, 0, media.Codec.BUFFER_FLAG_END_OF_STREAM);
        } else {
          const presentationTime = this.extractor.getSampleTime();
          this.codec.queueInputBuffer(inputBuffer, 0, sampleSize, presentationTime, 0);
          this.extractor.advance();
        }
      });
      
      this.codec.on('outputAvailable', (outputBuffer, bufferInfo) => {
        // 获取解码后的帧数据
        const imageInfo = this.createImageFromBuffer(outputBuffer, bufferInfo);
        resolve(imageInfo);
        
        this.codec.releaseOutputBuffer(outputBuffer, false);
      });
      
      this.codec.start();
    });
  }
}

详细步骤:

步骤1:配置权限

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "需要读取视频文件",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "需要保存提取的图片",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

步骤2:实现帧提取工具类

// utils/VideoFrameExtractor.ts
import media from '@ohos.multimedia.media';
import image from '@ohos.multimedia.image';
import fileio from '@ohos.fileio';
import featureAbility from '@ohos.ability.featureAbility';

export class VideoFrameExtractor {
  private static instance: VideoFrameExtractor;
  
  static getInstance(): VideoFrameExtractor {
    if (!this.instance) {
      this.instance = new VideoFrameExtractor();
    }
    return this.instance;
  }
  
  /**
   * 提取视频帧
   * @param videoPath 视频文件路径
   * @param timestamp 时间戳(微秒)
   * @param outputPath 输出图片路径
   */
  async extractFrame(
    videoPath: string, 
    timestamp: number, 
    outputPath: string
  ): Promise<boolean> {
    try {
      console.info(`开始提取帧: ${videoPath}, 时间戳: ${timestamp}`);
      
      // 1. 创建媒体提取器
      const extractor = await media.createMediaExtractor();
      await extractor.setDataSource(videoPath);
      
      // 2. 查找视频轨道
      const videoTrack = await this.findVideoTrack(extractor);
      if (!videoTrack) {
        throw new Error('未找到视频轨道');
      }
      
      // 3. 选择轨道并定位
      extractor.selectTrack(videoTrack.index);
      extractor.seekTo(timestamp, media.MediaExtractor.SeekMode.SEEK_TO_CLOSEST_SYNC);
      
      // 4. 创建解码器
      const decoder = await this.createDecoder(videoTrack.format);
      
      // 5. 解码并提取帧
      const frameData = await this.decodeFrame(extractor, decoder);
      
      // 6. 保存为图片
      await this.saveFrameAsImage(frameData, outputPath);
      
      console.info('帧提取成功:', outputPath);
      return true;
      
    } catch (error) {
      console.error('帧提取失败:', error);
      return false;
    }
  }
  
  private async findVideoTrack(extractor: media.MediaExtractor): Promise<{index: number, format: media.MediaFormat} | null> {
    const trackCount = extractor.getTrackCount();
    
    for (let i = 0; i < trackCount; i++) {
      const format = extractor.getTrackFormat(i);
      const mime = format.getString(media.MediaFormat.MIME);
      
      if (mime && mime.startsWith('video/')) {
        return { index: i, format };
      }
    }
    
    return null;
  }
  
  private async createDecoder(format: media.MediaFormat): Promise<media.Codec> {
    const mime = format.getString(media.MediaFormat.MIME);
    const decoder = await media.createDecoder(mime);
    
    // 获取视频尺寸
    const width = format.getInteger(media.MediaFormat.WIDTH);
    const height = format.getInteger(media.MediaFormat.HEIGHT);
    
    console.info(`视频尺寸: ${width}x${height}`);
    
    return decoder;
  }
  
  private async decodeFrame(
    extractor: media.MediaExtractor, 
    decoder: media.Codec
  ): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      let frameData: Uint8Array | null = null;
      
      decoder.on('error', (err) => {
        reject(err);
      });
      
      decoder.on('outputAvailable', (outputBuffer, bufferInfo) => {
        // 获取解码后的帧数据
        if (bufferInfo.size > 0) {
          frameData = new Uint8Array(bufferInfo.size);
          outputBuffer.read(frameData, 0, bufferInfo.size);
        }
        
        decoder.releaseOutputBuffer(outputBuffer, false);
        
        // 如果是关键帧,完成解码
        if (bufferInfo.flags & media.Codec.BUFFER_FLAG_KEY_FRAME) {
          if (frameData) {
            resolve(frameData);
          } else {
            reject(new Error('未能获取帧数据'));
          }
        }
      });
      
      decoder.start();
      
      // 输入数据循环
      const inputLoop = async () => {
        try {
          const inputBuffer = await this.getInputBuffer(decoder);
          const sampleSize = extractor.readSampleData(inputBuffer, 0);
          
          if (sampleSize < 0) {
            // 输入结束
            decoder.queueInputBuffer(
              inputBuffer, 
              0, 
              0, 
              0, 
              media.Codec.BUFFER_FLAG_END_OF_STREAM
            );
          } else {
            const presentationTime = extractor.getSampleTime();
            decoder.queueInputBuffer(
              inputBuffer, 
              0, 
              sampleSize, 
              presentationTime, 
              0
            );
            extractor.advance();
            
            // 继续循环
            setTimeout(inputLoop, 0);
          }
        } catch (error) {
          reject(error);
        }
      };
      
      inputLoop();
    });
  }
  
  private async getInputBuffer(decoder: media.Codec): Promise<media.Buffer> {
    return new Promise((resolve) => {
      decoder.on('inputAvailable', (inputBuffer) => {
        resolve(inputBuffer);
      });
    });
  }
  
  private async saveFrameAsImage(
    frameData: Uint8Array, 
    outputPath: string
  ): Promise<void> {
    // 创建图片对象
    const imagePacker = image.createImagePacker();
    
    // 假设frameData是RGB格式,需要转换为鸿蒙支持的格式
    const imageInfo = {
      size: { width: 1920, height: 1080 },
      pixelFormat: image.PixelMapFormat.RGBA_8888,
      colorSpace: image.ColorSpace.SRGB
    };
    
    const pixelMap = await image.createPixelMap(frameData, imageInfo);
    
    // 打包为JPEG
    const packOpts = { format: 'image/jpeg', quality: 95 };
    const packedData = await imagePacker.packing(pixelMap, packOpts);
    
    // 保存文件
    const file = await fileio.open(outputPath, fileio.OpenMode.CREATE | fileio.OpenMode.WRITE);
    await fileio.write(file.fd, packedData);
    await fileio.close(file.fd);
  }
}

步骤3:使用示例

// pages/VideoFrameExtract.ets
import { VideoFrameExtractor } from '../utils/VideoFrameExtractor';

@Entry
@Component
struct VideoFrameExtractPage {
  @State extractProgress: number = 0;
  @State isExtracting: boolean = false;
  @State extractedFrames: string[] = [];
  
  build() {
    Column({ space: 20 }) {
      Text('视频帧提取工具')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      Button('选择视频并提取帧')
        .onClick(() => this.selectAndExtractFrames())
        .enabled(!this.isExtracting)
      
      if (this.isExtracting) {
        Progress({ value: this.extractProgress, total: 100, type: ProgressType.Circular })
        Text(`提取进度: ${this.extractProgress}%`)
      }
      
      List({ space: 10 }) {
        ForEach(this.extractedFrames, (framePath: string) => {
          ListItem() {
            Image(framePath)
              .width(150)
              .height(100)
              .objectFit(ImageFit.Cover)
          }
        })
      }
      .width('100%')
      .height(300)
    }
    .padding(20)
  }
  
  async selectAndExtractFrames() {
    try {
      this.isExtracting = true;
      
      // 选择视频文件
      const videoPath = await this.selectVideoFile();
      if (!videoPath) return;
      
      // 获取视频信息
      const videoInfo = await this.getVideoInfo(videoPath);
      console.info(`视频时长: ${videoInfo.duration}ms`);
      
      // 提取多个关键帧
      const frameCount = 5;
      const interval = videoInfo.duration / (frameCount + 1);
      
      for (let i = 0; i < frameCount; i++) {
        const timestamp = interval * (i + 1);
        const outputPath = `${getContext().filesDir}/frame_${i}.jpg`;
        
        const success = await VideoFrameExtractor.getInstance().extractFrame(
          videoPath,
          timestamp * 1000, // 转换为微秒
          outputPath
        );
        
        if (success) {
          this.extractedFrames.push(outputPath);
        }
        
        this.extractProgress = Math.round(((i + 1) / frameCount) * 100);
      }
      
      console.info('帧提取完成');
      
    } catch (error) {
      console.error('提取失败:', error);
      AlertDialog.show({ message: '帧提取失败: ' + error.message });
    } finally {
      this.isExtracting = false;
      this.extractProgress = 0;
    }
  }
  
  async selectVideoFile(): Promise<string> {
    // 实现文件选择逻辑
    return new Promise((resolve) => {
      // 这里应该调用文件选择器
      resolve('/path/to/selected/video.mp4');
    });
  }
  
  async getVideoInfo(videoPath: string): Promise<{duration: number}> {
    // 获取视频信息
    return { duration: 10000 }; // 10秒示例
  }
}

其他解决方式:

1. 使用ImageCreator创建帧动画

// 使用ImageCreator批量处理帧
import image from '@ohos.multimedia.image';

export class FrameAnimationCreator {
  async createFrameAnimation(videoPath: string, fps: number = 10) {
    const imageCreator = image.createImageCreator(1920, 1088, 
      image.ImageFormat.YCBCR_420_SP, 30);
    
    imageCreator.on('imageAvailable', (image) => {
      // 处理每一帧
      this.processFrame(image);
    });
    
    // 配置帧提取参数
    const config = {
      fps: fps,
      quality: 0.8,
      format: image.PixelMapFormat.RGBA_8888
    };
    
    return imageCreator;
  }
}

2. 使用第三方FFmpeg库

// 集成FFmpeg for HarmonyOS
import ffmpeg from '@ohos.ffmpeg';

export class FFmpegFrameExtractor {
  async extractFrame(videoPath: string, time: string): Promise<string> {
    const outputPath = `${getContext().filesDir}/frame.jpg`;
    
    // FFmpeg命令提取帧
    const cmd = `-i ${videoPath} -ss ${time} -vframes 1 -q:v 2 ${outputPath}`;
    
    return new Promise((resolve, reject) => {
      ffmpeg.execute(cmd, (result) => {
        if (result === 0) {
          resolve(outputPath);
        } else {
          reject(new Error('FFmpeg帧提取失败'));
        }
      });
    });
  }
}

3. 使用系统媒体库扫描

// 使用系统媒体库获取缩略图
import mediaLibrary from '@ohos.multimedia.medialibrary';

export class SystemThumbnailExtractor {
  async getVideoThumbnail(videoUri: string): Promise<string> {
    const media = mediaLibrary.getMediaLibrary();
    
    // 获取系统生成的缩略图
    const fetchOp = {
      selections: `${mediaLibrary.FileKey.URI} = ?`,
      selectionArgs: [videoUri],
      columns: [mediaLibrary.FileKey.THUMBNAIL]
    };
    
    const result = await media.getFileAssets(fetchOp);
    if (result.fileAssets.length > 0) {
      return result.fileAssets[0].thumbnail;
    }
    
    return '';
  }
}

4. 服务端处理方案

// 上传视频到服务端处理
export class ServerFrameExtractor {
  async extractFrameOnServer(videoPath: string, timestamp: number): Promise<string> {
    // 上传视频文件
    const uploadTask = uni.uploadFile({
      url: 'https://your-server.com/api/extract-frame',
      filePath: videoPath,
      name: 'video',
      formData: {
        timestamp: timestamp.toString(),
        format: 'jpg'
      }
    });
    
    const result = await uploadTask;
    const response = JSON.parse(result.data);
    
    // 下载提取的帧
    const framePath = `${getContext().filesDir}/server_frame.jpg`;
    await this.downloadFile(response.frameUrl, framePath);
    
    return framePath;
  }
}

若是有更好方案,欢迎大佬们一起参与讨论。

在HarmonyOS Next中,使用@ohos.multimedia.mediaLibrary@ohos.image模块获取视频特定帧图片。首先通过mediaLibrary.getMediaLibrary获取媒体库实例,用getFileAssets查询视频文件。然后通过image.createImageCreator建立ImageCreator实例,调用createVideoThumbnail方法生成指定时间戳的缩略图,返回PixelMap对象。最后可将其转换为PixelMap用于显示或保存。该过程直接使用ArkTS API实现,无需依赖Java或C。

在HarmonyOS Next中,可以通过VideoPlayerImage组件结合实现视频特定帧的提取。具体步骤如下:

  1. 使用VideoPlayer加载视频:通过VideoPlayer组件加载目标视频文件,并获取其控制器。
  2. 定位到指定时间点:调用控制器的seekTo方法,将播放进度跳转到目标帧对应的时间位置。
  3. 捕获当前帧:利用VideoPlayergetCurrentFrame方法获取当前帧的PixelMap对象。
  4. 转换为Image对象:将PixelMap通过Image组件渲染或保存为图片文件。

示例代码片段:

import { VideoPlayer, media } from '@kit.MediaKit';

// 初始化VideoPlayer
let videoPlayer: media.VideoPlayer = await media.createVideoPlayer(...);

// 跳转到特定时间(例如第5秒)
await videoPlayer.seekTo(5 * 1000); // 单位毫秒

// 获取当前帧的PixelMap
let frame: image.PixelMap = await videoPlayer.getCurrentFrame();

// 使用Image组件显示或保存
// 例如:Image.src = frame

注意事项:

  • 确保视频已加载完成再执行跳转操作
  • 帧捕获精度受视频编码和关键帧间隔影响
  • 需申请ohos.permission.READ_MEDIA权限读取视频文件

这种方法适用于快速提取预览帧,若需高精度逐帧处理,建议使用AVMetadataExtractor解析视频元数据。

回到顶部