| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609 |
- 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<void>((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<boolean> {
- try {
- // 媒体入库
- const table: MediaTable = new MediaTable(context);
- // 等待数据库连接就绪(修复回调转Promise逻辑)
- await new Promise<void>((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<void>((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<boolean> {
- 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 '高质量(音质好,文件较大)';
- }
- }
|