flutter使用tauri实现一个一键视频转4K软件

作者:天平 时间:2022-10-23 05:46:42 

前言

先说结论,tauri是一个非常优秀的前端桌面开发框架,但是,rust门槛太高了。

一开始我是用electron来开发的,但是打包后发现软件运行不是很流畅,有那么一点卡顿。于是为了所谓的性能,我尝试用tauri来做我的软件。在学了两星期的rust后,我发现rust真的太难学了,最后硬是边做边查勉强做出来了。

软件运行起来是比electron做的丝滑很多,但是写rust真的是很痛苦,rust的写法和其他语言是截然不同的,在不知道之前我是个rust吹,觉得rust就是牛逼,真的上手后发现rust的门槛真的太高了,各种逆天的写法直接把我劝退,tauri无缝衔接前端真的又很爽。

如果golang也出一个和tauri一样的桌面端框架,那么golang将会是未来开发中的不二语言。

开发原因

我平时喜欢看一些动漫视频或者是收藏一些做得不错的动漫MAD,但是有时候因为番剧出的年代久远的问题,就算找最高清的资源,视频也不过720P,又或者是在b站上看一些动漫MAD的时候,up主虽然用的转场技巧比较不错,但是使用的动漫素材的质量比较差,十分可惜。

于是我想能不能做一个视频转4K的软件?类似于修复视频的功能。虽然网络上的修复视频软件有很多了,但是效果还是达不到我的要求,于是说干就干。

工作原理

视频其实就是一帧一帧的图片组成,如果要把视频转成4K,那么只要把视频分解成图片,再将图片转4K图片,最后将4K图片合并成4K视频就可以了。
于是我搜了一圈,了解到有Real-ESRGAN]这样的一个将图片转成4K的软件。并且里面也提供好了视频转4K的案例。

先用ffmpeg将视频分解成图片:

ffmpeg -i 原视频 -qscale:v 1 -qmin 1 -qmax 1 -vsync 0 临时图片路径/frame%08d.png

再用Real-ESRGAN将图片转4K图片:

./realesrgan-ncnn-vulkan.exe -i 临时图片目录 -o 4K图片目录 -n realesr-animevideov3 -s 2 -f jpg

最后查看原视频的帧数,然后用ffmpeg将4K图片合成4K视频:

ffmpeg -i 原视频
ffmpeg -r 23.98 -i 4K图片路径/frame%08d.jpg -c:v libx264 -r 帧数 -pix_fmt yuv420p 4K视频

只不过这样操作起来非常繁琐,并且只能一个个转,不能批量操作,也不能看到进度。虽然可以写一个cmd脚本批量操作,但是看不到进度,体验不是很好的。于是说干就干,开发软件!

开发过程

tauri提供了一些后端的操作权限给前端,也就是在前端就能完成读写文件,这就非常方便了!但是也是有一定限制的,比如要读取任意文件,就要rust去操作。

前提工作先准备一个文件,导出一个数组,pids,以便关闭软件时杀死所有windows进程。

export const pids: number[] = []

首先是创建3个文件夹,临时图片文件夹,图片转4K图片文件夹,输出视频文件夹,用来存放输出的资源的:

await readDir(`${basePath.value}/img_temp`).catch(() => {
       createDir(`${basePath.value}/img_temp`)
   })
   await readDir(`${basePath.value}/img_out`).catch(() => {
       createDir(`${basePath.value}/img_out`)
   })
   await readDir(`${basePath.value}/output`).catch(() => {
       createDir(`${basePath.value}/output`)
   })

然后是选定一个input文件夹,然后读取文件夹下面的视频,是rust实现:

fn read_dir_file(path: String) -> Vec<String> {
   let mut arr: Vec<String> = vec![];
   for item in read_dir(path).unwrap() {
       if !item.as_ref().unwrap().path().is_dir() {
           arr.push(item.unwrap().file_name().into_string().unwrap());
       }
   }
   return arr;
}

因为返回的是一个数组,前端获取到之后,就遍历去操作。但后面为了方便,我则是遍历这个数组,然后创建子组件,传入文件路径,让子组件去操作:

import { invoke } from '@tauri-apps/api/tauri'
const fileList = await invoke<string[]>('read_dir_file', { path: path.value })

首先还是遍历创建子文件夹,方便管理:

await readDir(`${props.basePath}/img_temp/${fileName}`).catch(() => {
       createDir(`${props.basePath}/img_temp/${fileName}`)
   })
   await readDir(`${props.basePath}/img_out/${fileName}`).catch(() => {
       createDir(`${props.basePath}/img_out/${fileName}`)
   })

接着调用tauri提供的shell指令: 不过在此之前,先要配置tauri.conf.json,让tauri支持任意命令

"tauri": {
       "allowlist": {
           "shell": {
               "scope": [
                   {
                       "name": "ffmpeg",
                       "cmd": "cmd",
                       "args": ["/C", { "validator": "\\S+" }]
                   }
               ]
           },
       },
   }

然后先执行读取视频的信息得到帧数和视频的总秒数,以便计算进度,并且把返回的pid存到数组中:

import { Command } from '@tauri-apps/api/shell'
const fps = ref('5')
const duration = ref('')
const cmd1 = `ffmpeg -i ${props.basePath}/input/${props.file}`
const command1 = new Command('ffmpeg', ['/C', cmd1])
const child1 = await command1.spawn()
   command1.stderr.on('data', (line) => {
       const fpsResult = line.match(/\w{2}\.?\w{0,2}(?= fps)/)
       /** 匹配视频持续时间的信息 */
       const durationResult = line.match(/(?<=Duration: ).+(?=, start)/)
       if (fpsResult) {
           fps.value = fpsResult[0]
           console.log('fps', fps.value)
       }
       if (durationResult) {
           duration.value = durationResult[0]
           console.log('duration', duration.value)
       }
   })
   pids.push(child1.pid)

用正则匹配帧数和持续时间,存到变量中。在命令执行完毕后,接着执行将视频分解图片的任务:

command1.on('close', async () => {
const cmd2 = `${props.ffmpegPath}  -i ${props.basePath}/input/${props.file} -qscale:v 1 -qmin 1 -qmax 1 -vsync 0 ${props.basePath}/img_temp/${fileName}/frame%08d.png`
   const command2 = new Command('ffmpeg', ['/C', cmd2])
   const child2 = await command2.spawn()
   pids.push(child2.pid)
})

至于监听进度,图片的总数是可以通过帧数和视频总秒数计算出来的,总秒数乘以帧数,就是要转换的图片总数。由于得到的持续时间是'00:04:32'这种格式的,先写一个函数将时间转成秒数:

/**
* @description  将字符串的时间转成总秒数的时间 00:04:35
* @param time   字符串的时间
* @returns      返回秒数的时间
*/
export function formatTime(time: string) {
   const hours = Number(time.split(':')[0])
   const mimutes = Number(time.split(':')[1])
   const seconds = Number(time.split(':')[2])
   return hours * 60 * 60 + mimutes * 60 + seconds
}

总图片就可以计算出来了,然后在输出时,使用节流,每隔1秒读取一次该文件夹下面的图片数量,则进度就是当前的图片数量/图片总数。

读取文件数量需要rust操作"

fn read_dir_file_count(path: String) -> i32 {
   let dir = read_dir(path).unwrap();
   let mut count: i32 = 0;
   for _ in dir {
       count += 1;
   }
   return count;
}

则整体是:

const total = formatTime(duration.value) * Number(fps.value)
           command2.stderr.on('data', async (line) => {
               const current = await invoke<number>('read_dir_file_count', {
                   path: `${props.basePath}/img_temp/${fileName}`,
               })
               console.log(current, total)
               precent1.value = Math.round((current / total) * 100)
           })

precent1就是绑定的进度条的变量。

在任务关闭后,执行优化图片的命令:

command2.on('close', async () => {
   const cmd3 = `${props.realesrgan} -i ${props.basePath}/img_temp/${fileName} -o ${props.basePath}/img_out/${fileName} -n realesr-animevideov3 -s 2 -f jpg`
   const command3 = new Command('ffmpeg', ['/C', cmd3])
   const child3 = await command3.spawn()
   pids.push(child3.pid)
})

监听转换的进度仍是读取文件夹下面当前的图片数量,用节流函数,优化性能:

command3.stderr.on('data', throttle(fn, 2000))
               async function fn() {
                   const current = await invoke<number>('read_dir_file_count', {
                       path: `${props.basePath}/img_out/${fileName}`,
                   })
                   precent2.value = Math.round((current / total) * 100)
                   console.log(current, total, (current / total) * 100)
                   // console.log(line)
}

最后在命令完成后,执行4K图片转4K视频的命令:

command3.on('close', async () => {
const cmd4 = `${props.ffmpegPath}  -r ${fps.value} -i  ${props.basePath}/img_out/${fileName}/frame%08d.jpg -i  ${props.basePath}/input/${props.file} -map 0:v:0 -map 1:a:0 -c:a copy -c:v ${props.model} -r ${fps.value} -pix_fmt yuv420p ${props.basePath}/output/${props.file}`
const command4 = new Command('ffmpeg', ['/C', cmd4])
const child4 = await command4.spawn()
pids.push(child4.pid)
})

监听进度此时则是去获取stderr输出的信息,然后匹配到当前转换的时间,再除以总时间

const total = formatTime(duration.value)
command4.stderr.on('data', throttle(fn, 200))
                   async function fn(data: string) {
                       /** 控制台的信息 */
                       const result = data.match(/(?<=time=).+(?= bitrate)/)
                       if (result) {
                           const current = formatTime(result[0])
                           console.log(current, total)
                           precent3.value = Math.round((current / total) * 100)
                       }
                   }

最后,如果关闭软件时,则是先把所有的任务都杀死,再关闭:

async function closeApp() {
   await Promise.all(
       pids.map(async (pid) => {
           return new Promise((resolve) => {
               const cmd = `taskkill /f /t /pid ${pid}`
               const command = new Command('ffmpeg', ['/C', cmd])
               command.spawn()
               command.on('close', () => {
                   resolve(0)
               })
           })
       })
   )
   appWindow.close()
}

以下是我的演示视频,不过文件有点大

www.bilibili.com/video/BV1EW&hellip;

这是我的项目地址:github.com/Minori-ty/m&hellip;

软件也已经打包好了,开箱即用github.com/Minori-ty/m&hellip;

来源:https://juejin.cn/post/7138225867565563917

标签:flutter,tauri,视频转4K
0
投稿

猜你喜欢

  • Android应用中加入微信分享简单方法

    2022-06-05 00:17:27
  • Java数据结构之链表相关知识总结

    2023-11-02 00:29:28
  • 详解Java中日志跟踪的简单实现

    2023-03-28 00:18:48
  • Android创建淡入淡出动画的详解

    2022-12-28 00:12:12
  • unity 实现摄像机绕某点旋转一周

    2021-06-11 16:48:57
  • Java实现调用对方http接口得到返回数据

    2023-02-27 22:36:29
  • SpringBoot 创建web项目并部署到外部Tomcat

    2023-09-15 18:25:04
  • 深入浅析Spring 的aop实现原理

    2023-01-10 00:00:10
  • Java中i++与++i的区别和使用

    2022-03-20 18:08:18
  • Android实现应用内置语言切换功能

    2021-11-14 13:19:50
  • Android ViewPager导航小圆点实现无限循环效果

    2022-07-09 13:10:33
  • android 下载时文件名是中文和空格会报错解决方案

    2023-05-25 04:45:19
  • Java字符串split使用方法代码实例

    2023-02-06 18:55:31
  • 解决Springboot项目启动后自动创建多表关联的数据库与表的方案

    2023-11-24 01:11:27
  • MyBatis @Select注解介绍:基本用法与动态SQL拼写方式

    2023-07-17 05:56:43
  • C#取得Web程序和非Web程序的根目录的N种取法总结

    2023-07-16 07:37:32
  • C# 在PDF中创建和填充域

    2022-05-14 05:49:23
  • JAVA设置手动提交事务,回滚事务,提交事务的操作

    2022-07-20 08:07:40
  • Java 确保某个Bean类被最后执行的几种实现方式

    2021-09-28 09:53:49
  • android自定义组件实现仪表计数盘

    2023-12-23 21:27:41
  • asp之家 软件编程 m.aspxhome.com