HarmonyOS 鸿蒙Next中怎么获取视频特定帧图片
HarmonyOS 鸿蒙Next中怎么获取视频特定帧图片 如题,如何从视频中提取特定帧图片?
背景知识:
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:获取文件的缩略图,传入缩略图尺寸,使用promise方式返回异步结果。
| 获取方式 | 使用场景 | 目标场景 |
|---|---|---|
| AVImageGenerator.fetchFrameByTime | 视频编辑、逐帧分析、自定义时间点截图。 | 动态解码视频帧生成缩略图(支持任意时间点帧提取)。 |
| PhotoAsset.getThumbnail | 相册浏览、快速预览。 | 直接获取相册中图片/视频的预生成缩略图。 |
原理:鸿蒙媒体框架的帧提取机制
鸿蒙系统通过媒体编解码框架和图像处理管道实现视频帧提取。核心原理是利用AVCodec解码器读取视频流,通过时间戳定位到特定帧,然后使用Image组件将解码后的YUV/RGB数据转换为位图格式,最后通过image模块保存为图片文件。
报错原因:权限和编解码问题导致提取失败
- 权限不足:缺少
ohos.permission.READ_MEDIA和ohos.permission.WRITE_MEDIA权限 - 编解码器不支持:视频格式不被系统解码器支持
- 内存不足:高分辨率视频帧占用大量内存
- 时间戳定位失败:seek操作无法精确到指定帧
- Surface配置错误:图像转换过程中的像素格式不匹配
- 文件路径无效:输出路径不存在或无写入权限
解决方案:使用鸿蒙原生媒体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中,可以通过VideoPlayer和Image组件结合实现视频特定帧的提取。具体步骤如下:
- 使用VideoPlayer加载视频:通过
VideoPlayer组件加载目标视频文件,并获取其控制器。 - 定位到指定时间点:调用控制器的
seekTo方法,将播放进度跳转到目标帧对应的时间位置。 - 捕获当前帧:利用
VideoPlayer的getCurrentFrame方法获取当前帧的PixelMap对象。 - 转换为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解析视频元数据。


