import { common, ConfigurationConstant } from '@kit.AbilityKit'; import { CommonConstants } from '../common/constants/CommonConstants'; import { VideoItem } from '../viewmodel/VideoItem'; import { AppUtil, FileUtil, StrUtil } from '@pura/harmony-utils'; import { taskpool, util } from '@kit.ArkTS'; import { Utility } from '../common/util/Utility'; import MediaTable from '../common/util/MediaTable'; import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg'; import { BusinessError } from '@kit.BasicServicesKit'; import { PlayStatus } from '../common/PlayStatus'; import { generateOutputPath, getFileNameWithoutExtension, setRingTone } from './EditAudio'; import { SegmentButtonItemTuple, SegmentButtonOptions, SwipeRefresher } from '@kit.ArkUI'; import { SegmentButton } from '@kit.ArkUI'; import PermissionUtil from '../common/util/PermissionUtil' import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'; // 视频音频提取功能 @Component export struct ExtractAudioFromVideo { onResult = (_result: boolean, outPath: string) => { } onBack = () => { } onPlayOrPause = () => { } onSeeOutputPath = (outPutPath:string) => { } onSliderChange = (value:number) => { } @Prop currentPath: string; @State isEditing:boolean = false @Link progressValue: number; @Link currentTime: string ; @Link videoUrl: string ;//用于判断当期播放的歌曲是不是和剪辑的歌一样,如果不是 播放控制不跟随更新 @State PROGRESS_MAX_VALUE: number = 100; @Link CONTROL_PlayStatus: number; @State bundleName: string = ''; @State durationStr:string = '' @State outputPath: string = ''; @State inputPath: string = ''; @Prop mVideoItem: VideoItem; @StorageProp('topRectHeight') topRectHeight: number = 0; @State isDarkMode: boolean = false; @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT; @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; context = this.getUIContext().getHostContext() as common.UIAbilityContext; // 音频提取质量选项 @State extractionQuality: number = 2; // 0-低, 1-中, 2-高 @State audioFormat: number = 0; // 0-MP3, 1-WAV, 2-AAC @State isProcessing: boolean = false; // SegmentButton选项 @State audioFormatOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ buttons: [{ text: 'MP3' }, { text: 'WAV' }, { text: 'AAC' }] as SegmentButtonItemTuple, backgroundColor: $r('app.color.index_background'), selectedBackgroundColor:$r('app.color.start_window_background'), selectedFontColor: $r('app.color.text_color'), buttonPadding:{top:10,bottom:10}, multiply: false }); @State qualityOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ buttons: [{ text: '低质量' }, { text: '中等质量' }, { text: '高质量' }] as SegmentButtonItemTuple, backgroundColor: $r('app.color.index_background'), selectedBackgroundColor:$r('app.color.start_window_background'), selectedFontColor: $r('app.color.text_color'), buttonPadding:{top:10,bottom:10}, multiply: false }); @State @Watch('onAudioFormatChange') selectedAudioFormatIndex: number[] = [this.audioFormat]; @State @Watch('onQualityChange') selectedQualityIndex: number[] = [this.extractionQuality]; private table: MediaTable = new MediaTable(this.context); onColorModeChange() { this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK; } // 音频格式变化监听 onAudioFormatChange() { if (this.selectedAudioFormatIndex.length > 0) { this.audioFormat = this.selectedAudioFormatIndex[0]; this.outputPath = generateOutputPath(this.inputPath,'提取音频',this.currentPath, this.getAudioFormatExtension(this.audioFormat)) } } // 音质变化监听 onQualityChange() { if (this.selectedQualityIndex.length > 0) { this.extractionQuality = this.selectedQualityIndex[0]; } } // 添加generateNewPath函数 generateNewPath(inputPath: string, fileName: string): string { const dir = inputPath.substring(0, inputPath.lastIndexOf('/') + 1); const dotIndex = fileName.lastIndexOf('.'); const name = dotIndex === -1 ? fileName : fileName.substring(0, dotIndex); const ext = this.getAudioFormatExtension(this.audioFormat); return `${dir}${name}${ext}`; } // 获取音频格式扩展名 getAudioFormatExtension(format: number): string { switch (format) { case 0: return '.mp3'; case 1: return '.wav'; case 2: return '.aac'; default: return '.mp3'; } } async aboutToAppear() { this.bundleName = AppUtil.getBundleName(); const rawDuration = this.mVideoItem.duration || '00:00:00'; this.durationStr = rawDuration; await PermissionUtil.activatePermission(this.mVideoItem.filePath) this.outputPath = generateOutputPath(this.mVideoItem.filePath,'提取音频',this.currentPath, this.getAudioFormatExtension(this.audioFormat)) console.info('onecold this.outputPath ='+this.outputPath) await new Promise((resolve, reject) => { this.table.getRdbStore(this.context, (err:Error) => { err ? reject(err) : resolve(); }); }); } // 提取视频音频 async extractVideoAudio() { if (this.isProcessing) { return; } if(StrUtil.isEmpty(this.inputPath)){ this.inputPath =this.mVideoItem.filePath } if(FileUtil.accessSync(this.outputPath)){ this.outputPath = generateOutputPath(this.inputPath,'提取音频',this.currentPath, this.getAudioFormatExtension(this.audioFormat)) } this.isProcessing = true; try { const task = new taskpool.Task( extractAudioFromVideo, this.context, this.inputPath, this.outputPath, this.extractionQuality, this.audioFormat ); const result = await taskpool.execute(task, taskpool.Priority.HIGH); if (result) { await this.saveDb(this.context, this.outputPath) this.onResult(true, this.outputPath); this.showSuccess(); } else { this.onResult(false, this.outputPath); } } catch (error) { console.error('Extract video audio failed:', error); this.onResult(false, this.outputPath); } finally { this.isProcessing = false; } } async saveDb(context: Context, outputPath: string): Promise { try { // 媒体入库 const table: MediaTable = new MediaTable(context); // 等待数据库连接就绪(修复回调转Promise逻辑) await new Promise((resolve, reject) => { table.getRdbStore(context, (err: Error | null) => { // 补充err可能为null的类型定义 if (err) { reject(new Error(`获取数据库连接失败: ${err.message}`)); } else { resolve(); } }); }); // 获取媒体元数据 const mediaItem = await Utility.uriGetMusicAssetsFromFile( context, outputPath, CommonConstants.TYPE_LOCAL, true ); if (!mediaItem) { console.error(` 获取媒体元数据失败: ${outputPath}`); return false; } // 插入数据库(回调转Promise,确保异步完成) await new Promise((resolve, reject) => { table.insert( mediaItem, (err: Error | null) => { // 补充insert回调的错误参数 if (err) { reject(new Error(`数据库插入失败: ${err.message}`)); } else { resolve(); } }, '' // 空字符串参数(根据table.insert 定义传入,若无用可考虑移除) ); }); console.log(` 媒体入库成功: ${outputPath}`); return true; } catch (error) { console.error(` 媒体入库失败: ${error instanceof Error ? error.message : error}`); return false; } } showSuccess() { try { this.getUIContext().getPromptAction().showDialog({ title: '提取成功', message: '视频音频提取完成,保存路径为:\n\n' + this.outputPath + '\n\n', buttons: [ { text: '查看路径', color: '#000000' }, { text: '设为铃声', color: '#000000' }, ] }, (err, data) => { if (err) { console.error('showDialog err: ' + err); return; } // 根据点击的按钮索引处理不同逻辑 if (data.index === 1) { // "设为铃声"按钮的索引为1 setRingTone(this.context, this.outputPath,FileUtil.getFileName(this.outputPath)) }else if (data.index === 0){//查看路径 this.onSeeOutputPath(this.outputPath) } console.info('showDialog success callback, click button: ' + data.index); }); } catch (error) { let message = (error as BusinessError).message; let code = (error as BusinessError).code; console.error(`showDialog args error code is ${code}, message is ${message}`); } } build() { Column() { this.topTitleBar() this.buildContent() } .height('100%') .width('100%') .backgroundColor($r('app.color.index_background')) } @Builder buildContent() { Column({ space: 20 }) { // 文件信息显示 Row({ space: 10 }) { Image(this.mVideoItem.pixelMapPath) .height(55) .width(55) .alt($r('app.media.llq')) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.6}) .borderRadius(12) .clip(true) Column(){ Text(` ${this.mVideoItem.name || this.mVideoItem.fileName}`) .fontSize(16) .fontColor($r('app.color.text_color')) .textAlign(TextAlign.Start) Text(` ${this.mVideoItem.artist || this.mVideoItem.size}`) .fontSize(16) .fontColor($r('app.color.text_color')) .textAlign(TextAlign.Start) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .width('100%') } .width('90%') .margin({ top: 5}) .justifyContent(FlexAlign.Start) // 输出文件名设置 Column({ space: 10 }) { Text('输出文件名:') .fontSize(16) .width('90%') .textAlign(TextAlign.Start) .fontColor($r('app.color.text_color')) TextArea({ text: getFileNameWithoutExtension(this.outputPath) }) .height('auto') .fontSize(14) .maxLines(3) .fontColor($r('app.color.text_color')) .width('90%') .onEditChange((isEditing: boolean) => { console.info(`onecold isEditing ${isEditing}`); this.isEditing = isEditing; }) .onChange((val: string) => { if(this.isEditing){ console.info('onecold onChange val=' + val) this.outputPath = generateOutputPath(this.mVideoItem.filePath,'',this.currentPath, this.getAudioFormatExtension(this.audioFormat), val ) } }) } .width('100%') .margin({ top: 5, bottom: 5 }) .justifyContent(FlexAlign.Start) // 音频格式选择 Column({ space: 10 }) { Text('音频格式:') .fontSize(16) .width('90%') .textAlign(TextAlign.Start) SegmentButton({ options: this.audioFormatOptions, selectedIndexes: $selectedAudioFormatIndex }) .width('90%') // 音质选择 Column({ space: 10 }) { Text('音质质量:') .fontSize(16) .width('90%') .textAlign(TextAlign.Start) SegmentButton({ options: this.qualityOptions, selectedIndexes: $selectedQualityIndex }) .width('90%') this.playButton() // 处理按钮 Button({ type: ButtonType.Capsule, stateEffect: true }){ Row({ space: 8 }) { if (this.isProcessing) { LoadingProgress() .width(26) .color(Color.Blue) } Text(this.isProcessing ? '处理中...' : '提取音频') .fontSize(14) .fontColor(Color.White) } } .width(200) .height(45) .backgroundColor(this.isProcessing ? Color.Gray : this.themeColor) .borderRadius(20) .margin({ top: 20 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { if (!this.isProcessing) { this.extractVideoAudio(); } }) .enabled(!this.isProcessing) } .width('100%') .padding({ top: 20, bottom: 20 }) .alignItems(HorizontalAlign.Center) } } .width('100%') } @Builder playButton() { Row(){ Button({type:ButtonType.Circle,stateEffect:true}){ Column() { Image(this.CONTROL_PlayStatus === PlayStatus.PLAY&&this.videoUrl==this.mVideoItem.filePath ? $r('app.media.hm_pause') : $r('app.media.hm_play2')) .width(38) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .fillColor(Color.White) .aspectRatio(CommonConstants.ASPECT_RATIO) .onClick(async () => { this.onPlayOrPause() }) } } .backgroundColor(this.themeColor) Text(this.videoUrl==this.mVideoItem.filePath?this.currentTime:'00:00') .fontSize($r('app.float.slider_font_size')) .fontColor($r('app.color.text_color')) .margin(3) Slider({ value: this.videoUrl==this.mVideoItem.filePath?this.progressValue:0, min: 0, max: this.PROGRESS_MAX_VALUE, step: 1, style: SliderStyle.OutSet }) .width('600px') .trackThickness(3) .layoutWeight(1) .margin({ left:1 }) .showSteps(false) .showTips(true) .onChange((value: number, mode: SliderChangeMode) => { this.onSliderChange(value) }) Text(this.durationStr) .fontSize($r('app.float.slider_font_size')) .fontColor($r('app.color.text_color')) .margin(3) .margin({ left: 2 }) } .width('90%') .margin({top:10}) } @Builder topTitleBar(){ Column() { Row({ space: 15 }) { //左侧滑动按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.onBack() }) .attributeModifier(new ShadowModifier()) .zIndex(0) Text('提取视频音频') .margin({left:3,right:10}) .fontColor($r('app.color.text_color')) .fontSize(19) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) } } .padding({ top: this.topRectHeight, left: 10, right: 10,bottom:12 }) .width('100%') } } // 视频音频提取的核心方法 @Concurrent async function extractAudioFromVideo( context: Context, inputPath: string, outPath?: string, quality: number = 2, audioFormat: number = 0 ): Promise { try { inputPath = FileUtil.getFilePath(inputPath); // 生成输出路径 let outputPath = outPath ?? (() => { const dir = inputPath.substring(0, inputPath.lastIndexOf('/') + 1); const fullName = inputPath.substring(inputPath.lastIndexOf('/') + 1); const dotIndex = fullName.lastIndexOf('.'); const name = dotIndex === -1 ? fullName : fullName.substring(0, dotIndex); const ext = dotIndex === -1 ? '' : fullName.substring(dotIndex); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); return `${dir}${name}_${timestamp}_audio${ext}`; })(); console.info(`onecold 开始提取视频音频,输入: ${inputPath}, 输出: ${outputPath}`); // 根据音频格式和质量设置参数 let qualityParams: string[] = []; let outputExtension: string = ''; let codec: string = ''; // 设置音频格式相关参数 switch (audioFormat) { case 0: // MP3 codec = 'libmp3lame'; outputExtension = '.mp3'; break; case 1: // WAV codec = 'pcm_s16le'; outputExtension = '.wav'; break; case 2: // AAC codec = 'aac'; outputExtension = '.aac'; break; default: codec = 'libmp3lame'; outputExtension = '.mp3'; break; } // 确保输出路径有正确的扩展名 if (!outputPath.toLowerCase().endsWith(outputExtension)) { const basePath = outputPath.lastIndexOf('.') > outputPath.lastIndexOf('/') ? outputPath.substring(0, outputPath.lastIndexOf('.')) : outputPath; outputPath = `${basePath}${outputExtension}`; } // 根据质量设置参数 switch (quality) { case 0: // 低质量 qualityParams = ['-ar', '22050', '-b:a', '64k']; break; case 1: // 中等质量 qualityParams = ['-ar', '44100', '-b:a', '128k']; break; case 2: // 高质量 default: qualityParams = ['-ar', '48000', '-b:a', '192k']; break; } try { console.info(`onecold 提取视频音频,格式: ${audioFormat === 0 ? 'MP3' : audioFormat === 1 ? 'WAV' : 'AAC'}`); // 构建FFmpeg命令 - 提取音频流,不重新编码视频 const commands = [ 'ffmpeg', '-i', inputPath, '-vn', // 不处理视频流 '-acodec', codec, // 音频编码器 ...qualityParams, '-y', outputPath ]; // 执行 FFmpeg 命令 await FFmpeg.execute(commands, { logCallback: (logLevel, logMessage) => console.log(`[${logLevel}] 视频音频提取: ${logMessage}`), progressCallback: (message) => console.log(`[progress] 视频音频提取: ${JSON.stringify(FFProgressMessageParser.parse(message))}`), }); console.info(`onecold 视频音频提取成功: ${outputPath}`); return true; } catch (error) { console.error(`onecold 视频音频提取失败: ${error}`); return false; } } catch (error) { console.error(`onecold 主流程错误: ${error}`); return false; } } // 音频格式描述 function getAudioFormatDescription(format: number): string { switch (format) { case 0: return 'MP3(通用格式,文件较小)'; case 1: return 'WAV(无损格式,文件较大)'; case 2: default: return 'AAC(高质量,文件较小)'; } } // 音频质量描述 function getQualityDescription(quality: number): string { switch (quality) { case 0: return '低质量(文件小,适合语音)'; case 1: return '中等质量(平衡大小与音质)'; case 2: default: return '高质量(音质好,文件较大)'; } }