第2.9篇视频导出与本地保存——DocumentViewPicker难度⭐⭐ 进阶前置知识2.5 视频播放器集成涉及源文件products/default/src/main/ets/services/VideoExportService.ets前言在画伴梦工厂中用户创作的作品最终需要保存为视频文件。HarmonyOS 提供了DocumentViewPicker文档选择器和systemShare系统分享两大能力让开发者可以轻松实现保存到本地和分享给好友两个核心功能。本篇将深入VideoExportService的实现涵盖从 rawfile 资源读取、文件缓存、DocumentViewPicker 保存、到 systemShare 分享的完整链路。1. 准备工作PreparedVideoFile 接口在开始导出前先定义一个接口来统一描述待导出的视频文件exportinterfacePreparedVideoFile{path:string;// 本地文件系统路径uri:string;// 文件 URIfile:// 协议fileName:string;// 文件名}这个接口贯穿整个导出流程——无论是从 rawfile 读取还是从本地路径获取最终都统一为这个结构。2. 准备视频文件prepareVideoprepareVideo方法负责将视频源Resource 或文件路径转换为本地可操作的文件。exportclassVideoExportService{staticasyncprepareVideo(videoUri:Resource|string,title:string,rawFilePath:string):PromisePreparedVideoFile{constfileNameVideoExportService.buildFileName(title);// 情况一videoUri 是字符串本地路径或 URIif(typeofvideoUristring){constsourcePathVideoExportService.toLocalPath(videoUri);if(sourcePath!){return{path:sourcePath,uri:fileUri.getUriFromPath(sourcePath),fileName:fileName};}thrownewError(当前视频不是本地文件暂不能导出);}// 情况二videoUri 是 Resourcerawfile 资源constcontextgetContext()ascommon.UIAbilityContext;if(rawFilePath){thrownewError(当前视频资源缺少导出路径);}constcontentawaitcontext.resourceManager.getRawFileContent(rawFilePath);consttargetPathcontext.cacheDir/fileName;VideoExportService.writeUint8Array(targetPath,content);return{path:targetPath,uri:fileUri.getUriFromPath(targetPath),fileName:fileName};}}两种视频源的处理源类型场景处理方式string本地文件路径或file://URI直接使用toLocalPath转为本地路径Resourcerawfile 中的内置资源读取 rawfile 内容并写入缓存目录resourceManager.getRawFileContent当视频是 rawfile 资源时通过resourceManager.getRawFileContent(rawFilePath)读取文件的二进制内容constcontentawaitcontext.resourceManager.getRawFileContent(rawFilePath);rawFilePath是 rawfile 中的相对路径例如assets/videos/demo.mp4。fileUri.getUriFromPathfileUri.getUriFromPath将本地文件路径转换为标准 URI 格式C:/path/to/file.mp4 → file://C:/path/to/file.mp4这个 URI 格式是许多系统 API 所要求的。3. DocumentViewPicker 保存3.1 什么是 DocumentViewPickerDocumentViewPicker是 HarmonyOS 提供的系统文档选择器支持用户选择文件进行打开或保存操作。它的save方法会弹出系统文件保存对话框让用户选择保存位置。3.2 saveToLocal 实现staticasyncsaveToLocal(videoUri:Resource|string,title:string,rawFilePath:string):Promisestring{// 1. 准备视频文件constpreparedawaitVideoExportService.prepareVideo(videoUri,title,rawFilePath);// 2. 创建 DocumentViewPickerconstcontextgetContext()ascommon.UIAbilityContext;constdocumentPickernewpicker.DocumentViewPicker(context);// 3. 配置保存选项constoptions:picker.DocumentSaveOptions{newFileNames:[prepared.fileName],// 默认文件名fileSuffixChoices:[MP4|.mp4]// 限定文件类型};// 4. 打开系统保存对话框constsavedUrisawaitdocumentPicker.save(options);if(savedUris.length0){thrownewError(未选择保存位置);}// 5. 将缓存文件拷贝到用户选择的位置VideoExportService.copyFileToUri(prepared.path,savedUris[0]);returnsavedUris[0];}3.3 DocumentSaveOptions 配置constoptions:picker.DocumentSaveOptions{newFileNames:[prepared.fileName],// 建议的文件名fileSuffixChoices:[MP4|.mp4]// 文件类型过滤};配置项值说明newFileNames[kid_animation_1234567890.mp4]默认文件名用户可修改fileSuffixChoices[MP4.mp4’]fileSuffixChoices的格式为显示名称|.扩展名例如MP4|.mp4、PNG Image|.png。3.4 保存流程preparedVideo (缓存目录) │ ▼ DocumentViewPicker.save(options) │ ▼ 系统文件保存对话框 ← 用户选择位置 │ ▼ savedUris[0] (用户选择的 URI) │ ▼ copyFileToUri(prepared.path, savedUris[0]) │ ▼ 视频已保存到用户指定位置4. 文件操作实现4.1 writeUint8Array写入二进制数据privatestaticwriteUint8Array(path:string,content:Uint8Array):void{constfilefileIo.openSync(path,fileIo.OpenMode.CREATE|// 文件不存在则创建fileIo.OpenMode.TRUNC|// 文件存在则截断fileIo.OpenMode.READ_WRITE// 读写模式);try{constbuffercontent.buffer.slice(content.byteOffset,content.byteOffsetcontent.byteLength);fileIo.writeSync(file.fd,buffer);}finally{fileIo.closeSync(file);}}关键点OpenMode.CREATE | TRUNC | READ_WRITE创建文件、清空内容、读写模式。content.buffer.slice(...)从 Uint8Array 中提取底层的 ArrayBuffer 片段。使用try/finally确保资源释放。4.2 copyFileToUri跨 URI 拷贝privatestaticcopyFileToUri(sourcePath:string,targetUri:string):void{// 打开源文件只读constsourceFilefileIo.openSync(sourcePath,fileIo.OpenMode.READ_ONLY);try{conststatfileIo.statSync(sourceFile.fd);constbuffernewArrayBuffer(stat.size);constreadSizefileIo.readSync(sourceFile.fd,buffer);// 打开目标文件写入、截断consttargetFilefileIo.openSync(targetUri,fileIo.OpenMode.WRITE_ONLY|fileIo.OpenMode.TRUNC);try{if(readSizestat.size){fileIo.writeSync(targetFile.fd,buffer);}else{fileIo.writeSync(targetFile.fd,buffer.slice(0,readSize));}}finally{fileIo.closeSync(targetFile);}}finally{fileIo.closeSync(sourceFile);}}这段代码手动完成了读取 → 写入的拷贝流程openSync以只读模式打开源文件。statSync获取文件大小创建对应大小的ArrayBuffer。readSync读取文件内容到 buffer。openSync以写入模式打开目标 URITRUNC 表示覆盖已有内容。writeSync写入数据。双层try/finally确保两个文件描述符都被释放。5. 文件名处理5.1 构建文件名privatestaticbuildFileName(title:string):string{consttrimmedtitle.trim();constbaseNametrimmed?kid_animation// 默认名称:VideoExportService.sanitizeFileName(trimmed);// 清理后的标题returnbaseName_Date.now().toString().mp4;}文件名格式{作品标题}_{时间戳}.mp4示例我的小汽车_1689123456789.mp45.2 文件名清理privatestaticsanitizeFileName(value:string):string{letresult;for(leti0;ivalue.length;i){constcharvalue.charAt(i);if(char\\||char/||char:||char*||char?||char||char||char||char|){result_;// 非法字符替换为下划线}else{resultchar;}}returnresult;}Windows 文件系统不允许以下字符\ / : * ? |。sanitizeFileName将这些字符统一替换为_确保生成的文件名在任何文件系统中都合法。6. systemShare 系统分享除了保存到本地还支持通过系统分享面板将视频分享给其他应用。6.1 showSystemSharestaticasyncshowSystemShare(videoUri:Resource|string,title:string,story:string,rawFilePath:string):Promisevoid{constcontextgetContext()ascommon.UIAbilityContext;constdataawaitVideoExportService.buildSharedData(videoUri,title,story,rawFilePath);constcontrollernewsystemShare.ShareController(data);constoptions:systemShare.ShareControllerOptions{selectionMode:systemShare.SelectionMode.SINGLE,// 单应用分享previewMode:systemShare.SharePreviewMode.DETAIL// 详细预览};awaitcontroller.show(context,options);}6.2 构建分享数据staticasyncbuildSharedData(videoUri:Resource|string,title:string,story:string,rawFilePath:string):PromisesystemShare.SharedData{constpreparedawaitVideoExportService.prepareVideo(videoUri,title,rawFilePath);// 视频记录constvideoRecord:ShareVideoRecord{utd:uniformTypeDescriptor.UniformDataType.MPEG4,// MIME 类型uri:prepared.uri,// 视频 URItitle:title,description:story};constdatanewsystemShare.SharedData(videoRecord);// 文本记录同时分享作品描述consttextRecord:ShareTextRecord{utd:uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,content:title\nstory,title:title,description:story};data.addRecord(textRecord);returndata;}分享数据包含两条记录视频记录分享视频文件本身。文本记录分享作品的标题和故事描述。uniformTypeDescriptor.UniformDataType.MPEG4和PLAIN_TEXT用于标识数据类型系统分享面板会根据这些类型展示对应的目标应用。7. 调用示例Index.ets 中的整合在页面中保存和分享通过两个方法触发7.1 保存视频privateasyncsaveCurrentVideo():Promisevoid{if(this.exportBusy){return;}this.exportBusytrue;this.showNotice(正在准备保存视频);try{awaitVideoExportService.saveToLocal(this.getCurrentVideo(),// 视频源Resource | stringthis.getCurrentWorkTitle(),// 作品标题this.getCurrentRawVideoPath()// rawfile 路径);this.downloadedActivetrue;this.showNotice(视频已保存);}catch(error){this.showNotice(保存失败this.getErrorMessage(errorasError));}finally{this.exportBusyfalse;}}7.2 分享视频privateasyncshareCurrentVideo():Promisevoid{if(this.exportBusy){return;}this.exportBusytrue;this.showNotice(正在打开分享);try{awaitVideoExportService.showSystemShare(this.getCurrentVideo(),this.getCurrentWorkTitle(),this.getCurrentWorkStory(),this.getCurrentRawVideoPath());this.sharedActivetrue;this.showNotice(已打开分享面板);}catch(error){this.showNotice(分享失败this.getErrorMessage(errorasError));}finally{this.exportBusyfalse;}}两者都使用exportBusy状态锁防止重复操作并用try/catch/finally确保状态正确恢复。8. 完整流程总结┌─────────────────────────────────────────────────────────┐ │ 视频导出完整流程 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 视频源Resource / string │ │ │ │ │ ▼ │ │ prepareVideo() │ │ ├─ resourceManager.getRawFileContent() (若为 Resource) │ │ ├─ writeUint8Array() → 写入 cacheDir │ │ └─ fileUri.getUriFromPath() → 生成 URI │ │ │ │ │ ▼ │ │ PreparedVideoFile { path, uri, fileName } │ │ │ │ │ ├──→ saveToLocal() │ │ │ ├─ DocumentViewPicker.save(options) │ │ │ │ ├─ newFileNames: [视频名.mp4] │ │ │ │ └─ fileSuffixChoices: [MP4|.mp4] │ │ │ └─ copyFileToUri() → 保存到用户选择位置 │ │ │ │ │ └──→ showSystemShare() │ │ ├─ buildSharedData() │ │ │ ├─ SharedData(videoRecord) │ │ │ └─ addRecord(textRecord) │ │ └─ ShareController.show() → 系统分享面板 │ │ │ └─────────────────────────────────────────────────────────┘小结本篇我们实现了完整的视频导出与保存功能prepareVideo统一处理 Resource 和 string 两种视频源将视频写入缓存目录。DocumentViewPicker.save通过系统文件保存对话框让用户自由选择保存位置。DocumentSaveOptions配置默认文件名和文件类型过滤。fileIo 文件操作openSync/readSync/writeSync/closeSync完成文件拷贝。resourceManager.getRawFileContent读取 rawfile 资源内容。sanitizeFileName清理文件名中的非法字符。systemShare构建包含视频和文本的分享数据调用系统分享面板。至此画伴梦工厂从绘画输入、AI 生成、视频播放到视频导出的完整链路已经全部打通。下一篇预告敬请期待后续章节