Flutter调用HarmonyOS NEXT原生相机拍摄&相册选择照片视频

目录

1.项目背景

2.遇到的问题

3.开发准备

4.开发过程

首先创建注册调用鸿蒙原生的渠道

创建并初始化插件

绑定通道完成插件中的功能

5.具体步骤

根据传值判断是相册选取还是打开相机

相册选取照片或视频

相机拍摄照片或视频

调用picker拍摄接口获取拍摄的结果

视频封面缩略图处理

打包缩略图

路径处理

数据返回

6.Flutter调用HarmonyOS原生通过路径上传到服务器

完整代码:


1.项目背景

我们的移动端项目是使用Flutter开发,考虑到开发周期和成本,使用了HarmonyOSNEXT(后续简称:鸿蒙)的Flutter兼容库,再将部分三方库更新为鸿蒙的Flutter兼容库,本项目选择相册的图片视频,使用相机拍照拍视频我们使用的是调用Android和iOS的原生方法使用

2.遇到的问题

因为我们使用的是原生方法,所以鸿蒙也得开发一套原生的配合使用,虽然我们也发现鸿蒙的Flutter兼容库中有image_picker这个库,但是在实际线上运行中,部分机型是无法正常工作的,主要是国内厂商深度定制引起的,那根据设备类型判断在纯血鸿蒙手机上用image_picker也是可行的方案,考虑到这样不方便后期维护,所以还是打算使用Flutter通过通道的形式去调用鸿蒙原生方式来实现

3.开发准备

首先得将鸿蒙适配Flutter的SDK下载,具体步骤可以参考:Flutter SDK 仓库,也可以参考我的上一篇文章:Flutter适配HarmonyOS实践_flutter支持鸿蒙系统

4.开发过程

  1. 首先创建注册调用鸿蒙原生的渠道
  2. 创建并初始化插件
  3. 绑定通道完成插件中的功能

首先创建注册调用鸿蒙原生的渠道

使用了兼容库后,ohos项目中在entry/src/main/ets/plugins目录下会自动生成一个GeneratedPluginRegistrant.ets文件,里面会注册所有你使用的兼容鸿蒙的插件,但是我们不能在这里注册,因为每次build,他会根据Flutter项目中的pubspec.yaml文件中最新的插件引用去重新注册。

我们找到GeneratedPluginRegistrant的注册地:EntryAbility.ets,我们在plugins中创建一个FlutterCallNativeRegistrant.ets,将他也注册一下:

TypeScript 复制代码
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import FlutterCallNativeRegistrant from '../plugins/FlutterCallNativeRegistrant';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
    ///GeneratedPluginRegistrant是自动根据引入的插件库生成的,所以调用原生的插件必须新起文件进行单独注册
    FlutterCallNativeRegistrant.registerWith(flutterEngine,this)
  }
}

创建并初始化插件

创建FlutterCallNativePlugin插件在FlutterCallNativeRegistrant中初始化

TypeScript 复制代码
export default class FlutterCallNativeRegistrant {
  private channel: MethodChannel | null = null;
  private photoPlugin?:PhotoPlugin;
  static registerWith(flutterEngine: FlutterEngine) {
    try {
      flutterEngine.getPlugins()?.add(new FlutterCallNativePlugin());
    } catch (e) {
    }
  }

}

绑定通道完成插件中的功能

绑定MethodChannel定义2个执行方法来调用原生的相册选取照片视频,相机拍摄照片视频:selectPhoto和selectVideo

TypeScript 复制代码
import { FlutterPlugin, FlutterPluginBinding, MethodCall,
  MethodCallHandler,
  MethodChannel, MethodResult } from "@ohos/flutter_ohos";
import router from '@ohos.router';
import PhotoPlugin from "./PhotoPlugin";
import { UIAbility } from "@kit.AbilityKit";

export default class FlutterCallNativePlugin implements FlutterPlugin,MethodCallHandler{
  private channel: MethodChannel | null = null;
  private photoPlugin?:PhotoPlugin;
  getUniqueClassName(): string {
    return "FlutterCallNativePlugin"
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    switch (call.method) {
      case "selectPhoto":
          this.photoPlugin = PhotoPlugin.getInstance();
          this.photoPlugin.setDataInfo(call, result ,1)
          this.photoPlugin.openImagePicker();
          break;

      case "selectVideo":
          this.photoPlugin = PhotoPlugin.getInstance();
          this.photoPlugin.setDataInfo(call, result ,2)
          this.photoPlugin.openImagePicker();
        break;
    
      default:
        result.notImplemented();
        break;
    }
  }
  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_callNative");
    this.channel.setMethodCallHandler(this)
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

}

5.具体步骤

  • 根据传值判断是相册选取还是打开相机
  • 相册选取照片或视频
  • 相机拍摄照片或视频
  • 视频封面处理
  • 路径处理
  • 数据返回

根据传值判断是相册选取还是打开相机

TypeScript 复制代码
  openImagePicker() {
    if (this.type === 1) {
      this.openCameraTakePhoto()
    } else if (this.type === 2) {
      this.selectMedia()
    } else {
      this.selectMedia()
    }
  }

相册选取照片或视频

用户有时需要分享图片、视频等用户文件,开发者可以通过特定接口拉起系统图库,用户自行选择待分享的资源,然后最终完成分享。此接口本身无需申请权限,目前适用于界面UIAbility,使用窗口组件触发。

这个方式的好处显而易见,不像Android或者iOS还需要向用户申请隐私权限,在鸿蒙中,以下操作完全是系统级的,不需要额外申请权限

1.创建图片媒体文件类型文件选择选项实例

TypeScript 复制代码
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();

2.根据类型配置可选的媒体文件类型和媒体文件的最大数目等参数

TypeScript 复制代码
if (this.mediaType === 1) {
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE // 过滤选择媒体文件类型为IMAGE
    } else if (this.mediaType === 2) {
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE // 过滤选择媒体文件类型为VIDEO
    }
 photoSelectOptions.maxSelectNumber = this.mediaType === 2?1:this.maxCount; // 选择媒体文件的最大数目
    photoSelectOptions.isPhotoTakingSupported=false;//是否支持拍照
    photoSelectOptions.isSearchSupported=false;//是否支持搜索

还有其他可配置项请参考API文档

3创建图库选择器实例,调用PhotoViewPicker.select接口拉起图库界面进行文件选择。文件选择成功后,返回PhotoSelectResult结果集。

TypeScript 复制代码
let uris: Array<string> = [];
const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
  uris = photoSelectResult.photoUris;
  console.info('photoViewPicker.select to file succeed and uris are:' + uris);
}).catch((err: BusinessError) => {
  console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
})

打印相册选择图片和视频的结果:

TypeScript 复制代码
photoViewPicker.select to file succeed and uris 
are:file://media/Photo/172/IMG_1736574824_157/IMG_20250111_135204.jpg,file://media/Photo/164/IMG_1736514105_152/image_1736514005016.jpg
TypeScript 复制代码
photoViewPicker.select to file succeed and uris 
are:file://media/Photo/136/VID_1735732161_009/VID_20250101_194749.mp4

相机拍摄照片或视频

1.配置PickerProfile

说明

PickerProfile的saveUri为可选参数,如果未配置该项,拍摄的照片和视频默认存入媒体库中。

如果不想将照片和视频存入媒体库,请自行配置应用沙箱内的文件路径。

应用沙箱内的这个文件必须是一个存在的、可写的文件。这个文件的uri传入picker接口之后,相当于应用给系统相机授权该文件的读写权限。系统相机在拍摄结束之后,会对此文件进行覆盖写入

TypeScript 复制代码
 let pathDir = getContext().filesDir;
    let fileName = `${new Date().getTime()}`
    let filePath = pathDir + `/${fileName}.tmp`
    let result: picker.PickerResult
    fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);
    let uri = fileUri.getUriFromPath(filePath);
    let pickerProfile: picker.PickerProfile = {
      cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
      saveUri: uri
    };

调用picker拍摄接口获取拍摄的结果

TypeScript 复制代码
 if (this.mediaType === 1) {
      result =
        await picker.pick(getContext(), [picker.PickerMediaType.PHOTO],
          pickerProfile);
    } else if (this.mediaType === 2) {
      result =
        await picker.pick(getContext(), [picker.PickerMediaType.VIDEO],
          pickerProfile);
    }

console.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);

打印结果:

TypeScript 复制代码
picker resultCode: 0,resultUri: 
file://com.example.demo/data/storage/el2/base/haps/entry/files/1737443816605.tmp,mediaType: photo
TypeScript 复制代码
picker resultCode: 0,resultUri: 
file://com.example.demo/data/storage/el2/base/haps/entry/files/1737443929031.tmp,mediaType: video
复制代码
因为我们配置了saveUri,所以拍摄的图片视频是存在我们应用沙盒中。

视频封面缩略图处理

视频拿到一般都是直接上传,但是有的场景需要将适配封面也拿到,那么路径在沙盒中,就直接一次性处理好

1.创建AVImageGenerator对象

TypeScript 复制代码
// 创建AVImageGenerator对象
      let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()

2.根据传入的视频uri打开视频文件

TypeScript 复制代码
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);

3.将打开后的文件配置给avImageGenerator

TypeScript 复制代码
let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
      avImageGenerator.fdSrc = avFileDescriptor;

4.初始化参数

TypeScript 复制代码
  let timeUs = 0
      let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC
      let param: media.PixelMapParams = {
        width : 300,
        height : 400,
      }

5.异步获取缩略图

TypeScript 复制代码
 // 获取缩略图(promise模式)
      let pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)

6.缩放资源,并返回缩略图

TypeScript 复制代码
 avImageGenerator.release()
      console.info(`release success.`)
      fs.closeSync(file)
      return pixelMap

打包缩略图

1.创建imagePicker实例,该类是图片打包器类,用于图片压缩和打包

TypeScript 复制代码
const imagePackerApi: image.ImagePacker = image.createImagePacker();

2.创建配置image.PackingOption

TypeScript 复制代码
        let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }

3.将缩略图打包保存并返回文件路径

TypeScript 复制代码
imagePackerApi.packing(pixelMap, packOpts).then(async (buffer: ArrayBuffer) => {
            let fileName = `${new Date().getTime()}.tmp`
            // //文件操作
            let filePath = getContext().cacheDir + fileName
            let file = fileIo.openSync(filePath,fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
            fileIo.writeSync(file.fd,buffer)
            //获取uri
            let urlStr = fileUri.getUriFromPath(filePath)
            resolve(urlStr)
          })

路径处理

因为以上所有的路径都是在鸿蒙设备上的路径,Flutter的MultipartFile.fromFile(ipath)是无法读取纯血鸿蒙设备的路径

TypeScript 复制代码
01-16 16:23:46.805   17556-17654   A00000/com.gqs...erOHOS_Native  
flutter settings log message: 错误信息:PathNotFoundException: Cannot retrieve length of file, path = 'file://com.example.demo/data/storage/el2/base/haps/entry/files/1737015822716.tmp' (OS Error: No such file or directory, errno = 2)

所以我们需要把路径转换一下:

TypeScript 复制代码
/*
 * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import util from '@ohos.util';
import Log from '@ohos/flutter_ohos/src/main/ets/util/Log';

const TAG = "FileUtils";

export default class FileUtils {
  static getPathFromUri(context: common.Context | null, uri: string, defExtension?: string) {
    Log.i(TAG, "getPathFromUri : " + uri);
    let inputFile: fs.File;
    try {
      inputFile = fs.openSync(uri);
    } catch (err) {
      Log.e(TAG, "open uri file failed err:" + err)
      return null;
    }
    if (inputFile == null) {
      return null;
    }
    const uuid = util.generateRandomUUID();
    if (!context) {
      return
    }
    {
      const targetDirectoryPath = context.cacheDir + "/" + uuid;
      try {
        fs.mkdirSync(targetDirectoryPath);
        let targetDir = fs.openSync(targetDirectoryPath);
        Log.i(TAG, "mkdirSync success targetDirectoryPath:" + targetDirectoryPath + " fd: " + targetDir.fd);
        fs.closeSync(targetDir);
      } catch (err) {
        Log.e(TAG, "mkdirSync failed err:" + err);
        return null;
      }

      const inputFilePath = uri.substring(uri.lastIndexOf("/") + 1);
      const inputFilePathSplits = inputFilePath.split(".");
      Log.i(TAG, "getPathFromUri inputFilePath: " + inputFilePath);
      const outputFileName = inputFilePathSplits[0];
      let extension: string;
      if (inputFilePathSplits.length == 2) {
        extension = "." + inputFilePathSplits[1];
      } else {
        if (defExtension) {
          extension = defExtension;
        } else {
          extension = ".jpg";
        }
      }
      const outputFilePath = targetDirectoryPath + "/" + outputFileName + extension;
      const outputFile = fs.openSync(outputFilePath, fs.OpenMode.CREATE);
      try {
        Log.i(TAG, "copyFileSync inputFile fd:" + inputFile.fd + " outputFile fd:" + outputFile.fd);
        fs.copyFileSync(inputFile.fd, outputFilePath);
      } catch (err) {
        Log.e(TAG, "copyFileSync failed err:" + err);
        return null;
      } finally {
        fs.closeSync(inputFile);
        fs.closeSync(outputFile);
      }
      return outputFilePath;
    }
  }

}

通过调用FileUtils的静态方法getPathFromUri,传入上下文和路径,就能获取到真正的SD卡的文件地址:

TypeScript 复制代码
/data/storage/el2/base/haps/entry/cache/53ee7666-7ba4-4f72-9d37-3c09111a2293/1737446424534.tmp

数据返回

TypeScript 复制代码
   let videoUrl =  this.retrieveCurrentDirectoryUri(uris[0])
   let coverImageUrl =  this.retrieveCurrentDirectoryUri(videoThumb)
   map.set("videoUrl", this.retrieveCurrentDirectoryUri(uris[0]));
   map.set("coverImageUrl", this.retrieveCurrentDirectoryUri(videoThumb));
   this.result?.success(map);

6.Flutter调用HarmonyOS原生通过路径上传到服务器

TypeScript 复制代码
上文中我们提到建立通道Channel
MethodChannel communicateChannel = MethodChannel("flutter_callNative");
final result = await communicateChannel.invokeMethod("selectVideo", vars);
if (result["videoUrl"] != null && result["coverImageUrl"] != null) {
    String? video = await FileUploader.uploadFile(result["videoUrl"].toString());
    String? coverImageUrl =await FileUploader.uploadFile(result["coverImageUrl"].toString());
}

完整代码:

TypeScript 复制代码
import { camera, cameraPicker as picker } from '@kit.CameraKit'
import { fileIo, fileUri } from '@kit.CoreFileKit'
import { MethodCall, MethodResult } from '@ohos/flutter_ohos';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError } from '@kit.BasicServicesKit';
import json from '@ohos.util.json';
import FileUtils from '../utils/FileUtils';
import HashMap from '@ohos.util.HashMap';
import media from '@ohos.multimedia.media';
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';

/**
 * @FileName : PhotoPlugin
 * @Author : kirk.wang
 * @Time : 2025/1/16 11:30
 * @Description :  flutter调用鸿蒙原生组件的选择相片、选择视频、拍照、录制视频
 */
export default class  PhotoPlugin {
  private imgSrcList: Array<string> = [];
  private call?: MethodCall;
  private result?: MethodResult;
  ///打开方式:1-拍摄,2-相册
  private type: number=0;
  ///最大数量
  private maxCount: number=0;
  ///资源类型:1-图片,2-视频,else 所有文件类型
  private mediaType: number=0;
  // 静态属性存储单例实例
  private static instance: PhotoPlugin;
  // 静态方法获取单例实例
  public static getInstance(): PhotoPlugin {
    if (!PhotoPlugin.instance) {
      PhotoPlugin.instance = new PhotoPlugin();
    }
    return PhotoPlugin.instance;
  }

  // 提供设置和获取数据的方法
  public setDataInfo(call: MethodCall, result: MethodResult, mediaType: number) {
    this.call = call;
    this.result = result;
    this.mediaType = mediaType;
    this.type = this.call.argument("type") as number;
    this.maxCount = call.argument("maxCount") as number;
  }

  openImagePicker() {
    if (this.type === 1) {
      this.openCameraTakePhoto()
    } else if (this.type === 2) {
      this.selectMedia()
    } else {
      this.selectMedia()
    }
  }

  selectMedia() {
    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    if (this.mediaType === 1) {
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE // 过滤选择媒体文件类型为IMAGE
    } else if (this.mediaType === 2) {
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE // 过滤选择媒体文件类型为VIDEO
    }
    photoSelectOptions.maxSelectNumber = this.mediaType === 2?1:this.maxCount; // 选择媒体文件的最大数目
    photoSelectOptions.isPhotoTakingSupported=false;//是否支持拍照
    photoSelectOptions.isSearchSupported=false;//是否支持搜索
    let uris: Array<string> = [];
    const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
    photoViewPicker.select(photoSelectOptions).then(async (photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      uris = photoSelectResult.photoUris;
      console.info('photoViewPicker.select to file succeed and uris are:' + uris);
      let jsonResult = "";
      if (this.mediaType === 1) {
        uris.forEach((uri => {
          this.imgSrcList.push(this.retrieveCurrentDirectoryUri(uri))
        }))
        jsonResult = json.stringify(this.imgSrcList)
        this.result?.success(jsonResult);
      } else if (this.mediaType === 2) {
          let map = new HashMap<string, string>;
          await this.getVideoThumbPath(uris[0]).then((videoThumb)=>{
            let videoUrl =  this.retrieveCurrentDirectoryUri(uris[0])
            let coverImageUrl =  this.retrieveCurrentDirectoryUri(videoThumb)
                map.set("videoUrl", videoUrl);
                map.set("coverImageUrl", coverImageUrl);
                this.result?.success(map);
          });
      }

      console.assert('result  success:'+jsonResult);
    }).catch((err: BusinessError) => {
      console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
    })
  }

  async openCameraTakePhoto() {
    let pathDir = getContext().filesDir;
    let fileName = `${new Date().getTime()}`
    let filePath = pathDir + `/${fileName}.tmp`
    let result: picker.PickerResult
    fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);
    let uri = fileUri.getUriFromPath(filePath);
    let pickerProfile: picker.PickerProfile = {
      cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
      saveUri: uri
    };
    if (this.mediaType === 1) {
      result =
        await picker.pick(getContext(), [picker.PickerMediaType.PHOTO],
          pickerProfile);
    } else if (this.mediaType === 2) {
      result =
        await picker.pick(getContext(), [picker.PickerMediaType.VIDEO],
          pickerProfile);
    } else if (this.mediaType === 3) {
      result =
        await picker.pick(getContext(), [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],
          pickerProfile);
    } else {
      result =
        await picker.pick(getContext(), [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],
          pickerProfile);
    }
    console.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);
    if (result.resultCode == 0) {
      if (result.mediaType === picker.PickerMediaType.PHOTO) {
        let imgSrc = this.retrieveCurrentDirectoryUri(result.resultUri);
        this.imgSrcList.push(imgSrc);
        this.result?.success(json.stringify(this.imgSrcList));
      } else {
           let map = new HashMap<string, string>;
          await this.getVideoThumbPath(result.resultUri).then((videoThumb)=>{
            if(videoThumb!==''){
              let videoUrl =  this.retrieveCurrentDirectoryUri(result.resultUri)
              let coverImageUrl =  this.retrieveCurrentDirectoryUri(videoThumb)
                map.set("videoUrl",videoUrl);
                map.set("coverImageUrl", coverImageUrl);
                this.result?.success(map);
            }
          });
      }
    }
  }

  retrieveCurrentDirectoryUri(uri: string): string {
    let realPath = FileUtils.getPathFromUri(getContext(), uri);
    return realPath ?? '';
  }

  async getVideoThumbPath(filePath:string) {

    return new Promise<string>((resolve, reject) => {
      setTimeout(() => {
        let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }
        const imagePackerApi = image.createImagePacker();
         this.getVideoThumb(filePath).then((pixelMap)=>{
          imagePackerApi.packing(pixelMap, packOpts).then(async (buffer: ArrayBuffer) => {
            let fileName = `${new Date().getTime()}.tmp`
            // //文件操作
            let filePath = getContext().cacheDir + fileName
            let file = fileIo.openSync(filePath,fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
            fileIo.writeSync(file.fd,buffer)
            //获取uri
            let urlStr = fileUri.getUriFromPath(filePath)
            resolve(urlStr)
          })
        })
      }, 0);
    });
  }
  ///获取视频缩略图
     getVideoThumb = async (filePath: string) => {
      // 创建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模式)
      let pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
      // 释放资源(promise模式)
      avImageGenerator.release()
      console.info(`release success.`)
      fs.closeSync(file)
      return pixelMap
    };

}

创作不易,如果我的内容帮助到了你,烦请小伙伴点个关注,留个言,分享给需要的人,不胜感激。

相关推荐
sunly_6 小时前
Flutter:carousel_slider 横向轮播图、垂直轮播公告栏实现
flutter
星释6 小时前
鸿蒙Flutter实战:17-无痛上架审核指南
flutter·华为·harmonyos
jikuaidi6yuan8 小时前
鸿蒙操作系统的安全架构
华为·harmonyos·安全架构
HarderCoder11 小时前
鸿蒙开发者认证-题库(二)
harmonyos
轻口味12 小时前
HarmonyOS Next 最强AI智能辅助编程工具 CodeGenie介绍
人工智能·华为·harmonyos·deveco-studio·harmonyos-next·codegenie
lichong95112 小时前
【Flutter&Dart】MVVM(Model-View-ViewModel)架构模式例子-http版本(30 /100)
android·flutter·http·架构·postman·win·smartapi
jikuaidi6yuan13 小时前
除了基本的事件绑定,鸿蒙的ArkUI
华为·harmonyos
GY-9313 小时前
Flutter中PlatformView在鸿蒙中的使用
flutter·harmonyos
开着拖拉机回家14 小时前
【Linux】华为服务器使用U盘安装统信操作系统
linux·服务器·华为·ibmc·ultraiso