Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vset",
"productName": "VSET",
"version": "4.3.6",
"version": "4.4.1",
"description": "VSET",
"author": "NangInShell",
"main": "./out/main/index.js",
Expand Down
Binary file modified resources/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 0 additions & 4 deletions src/main/childProcessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ function safeUnpipe(): void {
vspipe.proc.stdout.unpipe(ffmpeg.proc.stdin)
}
catch {}
try {
ffmpeg.proc.stdin.end()
}
catch {}
}
}

Expand Down
19 changes: 16 additions & 3 deletions src/main/getCorePath.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TaskConfig } from '@shared/type/taskConfig'
import { existsSync, mkdirSync } from 'node:fs'
import path from 'node:path'
import { app } from 'electron'

Expand Down Expand Up @@ -36,6 +37,18 @@ export function getExtraSRModelPath(): string {
return path.join(getCorePath(), 'vs-coreplugins', 'models', 'VSET_ExtraSrModel')
}

function ensureVpyDir(): string {
const dir = process.env.NODE_ENV === 'development'
? path.join(app.getAppPath(), 'resources', 'vpyFile')
: path.join(app.getAppPath(), '..', 'vpyFile')

if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}

return dir
}

/**
* 获取 VSET 生成的设置文件路径
* 暂时存放在 outputfolder 目录下
Expand All @@ -46,8 +59,8 @@ export function getGenSettingsPath(taskConfig: TaskConfig): string {

/**
* 获取 VSET 生成的 vpy 文件路径
* 暂时存放在 outputfolder 目录下
* 存放在软件 vpyFile 目录下
*/
export function getGenVpyPath(taskConfig: TaskConfig, baseName: string): string {
return path.join(taskConfig.outputFolder, `${baseName}.vpy`)
export function getGenVpyPath(_taskConfig: TaskConfig, baseName: string): string {
return path.join(ensureVpyDir(), `${baseName}.vpy`)
}
24 changes: 23 additions & 1 deletion src/main/getSystemInfo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { readdir } from 'node:fs/promises'
import si from 'systeminformation'
import { getExtraSRModelPath } from './getCorePath'

export async function getGpuInfo(): Promise<Array<string>> {
const deviceList: Array<string> = []
Expand All @@ -11,6 +13,26 @@ export async function getGpuInfo(): Promise<Array<string>> {

export async function getCpuInfo(): Promise<string> {
const cpu = await si.cpu()

return cpu.brand
}

export async function getMemoryInfo(): Promise<string> {
const mem = await si.mem()
const totalGB = (mem.total / (1024 * 1024 * 1024)).toFixed(2)
return `${totalGB} GB`
}

export async function getExtraSRModelList(): Promise<Array<string>> {
const modelDir = getExtraSRModelPath()
try {
const files = await readdir(modelDir)
// 过滤出 .onnx 文件并去掉扩展名
return files
.filter(file => file.endsWith('.onnx'))
.map(file => file.replace('.onnx', ''))
}
catch (error) {
console.error('Error reading extra SR model directory:', error)
return []
}
}
34 changes: 28 additions & 6 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,47 @@ import { IpcChannelInvoke, IpcChannelSend } from '@shared/constant/ipc'
import { app, BrowserWindow, ipcMain, nativeImage, shell } from 'electron'
import appIcon from '../../resources/icon.png?asset'
import { killAllProcesses } from './childProcessManager'
import { getCpuInfo, getGpuInfo } from './getSystemInfo'
import { getCpuInfo, getExtraSRModelList, getGpuInfo, getMemoryInfo } from './getSystemInfo'
import { openDirectory } from './openDirectory'
import { preview, previewFrame } from './previewOutput'
import { PauseCommand, runCommand } from './runCommand'
import { writeSettingsJson } from './writeFile'

function createWindow(): BrowserWindow {
const mainWindow = new BrowserWindow({
width: 900,
height: 700,
minWidth: 900,
minHeight: 670,
width: 1000,
height: 875,
minWidth: 1000,
minHeight: 875,
show: false,
autoHideMenuBar: true,
frame: false,
resizable: true,
icon: nativeImage.createFromPath(appIcon),
title: 'VSET 4.3.6',
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false,
},
})

// ipcMain

ipcMain.on('window:minimize', () => {
mainWindow?.minimize()
})

ipcMain.on('window:maximize', () => {
if (mainWindow?.isMaximized()) {
mainWindow.unmaximize()
}
Comment on lines +37 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

mainWindow.unmaximize() 的调用与 else 块中的 mainWindow?.maximize() 在可选链操作符 ?. 的使用上不一致。虽然在这里是安全的,但保持一致的风格可以提高代码的可读性和健壮性。

Suggested change
if (mainWindow?.isMaximized()) {
mainWindow.unmaximize()
}
if (mainWindow?.isMaximized()) {
mainWindow?.unmaximize()
}

else {
mainWindow?.maximize()
}
})

ipcMain.on('window:close', () => {
mainWindow?.close()
})
ipcMain.on(IpcChannelSend.EXECUTE_COMMAND, runCommand)

ipcMain.on(IpcChannelSend.PAUSE, PauseCommand)
Expand All @@ -45,6 +63,10 @@ function createWindow(): BrowserWindow {

ipcMain.handle(IpcChannelInvoke.GET_CPU_INFO, getCpuInfo)

ipcMain.handle(IpcChannelInvoke.GET_MEMORY_INFO, getMemoryInfo)

ipcMain.handle(IpcChannelInvoke.GET_EXTRA_SR_MODEL_LIST, getExtraSRModelList)

// mainWindow
mainWindow.on('ready-to-show', () => {
mainWindow?.show()
Expand Down
189 changes: 108 additions & 81 deletions src/main/runCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,118 @@ function generate_cmd(taskConfig: TaskConfig, hasAudio: boolean, hasSubtitle: bo
return cmd
}

// 新增:获取输入视频信息的独立函数
async function getInputVideoInfo(event: IpcMainEvent, video: string): Promise<{
hasAudio: boolean
hasSubtitle: boolean
videoStream: any
}> {
Comment on lines +78 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了提高代码的可读性和可维护性,建议为 ffprobemediainfo 的输出定义类型接口,而不是使用 any 类型。这可以提供更好的类型安全和自动补全支持。

例如,您可以定义以下接口:

interface FfprobeStream {
  codec_type?: 'video' | 'audio' | 'subtitle';
  nb_frames?: string;
  avg_frame_rate?: string;
  width?: number;
  height?: number;
  [key: string]: any;
}

interface MediaInfoTrack {
  '@type': 'Video' | 'Audio' | 'General' | string;
  FrameRate_Mode?: string;
  [key: string]: any;
}

然后更新 getInputVideoInfo 函数的返回类型:

async function getInputVideoInfo(video: string): Promise<{
  hasAudio: boolean
  hasSubtitle: boolean
  videoStream: FfprobeStream | undefined
  // ... 其他属性
}>

在函数实现中,您可以使用这些类型来查找流:
const videoStream = allStreams.find((s: FfprobeStream) => s.codec_type === 'video')

const ffprobePath = getExecPath().ffprobe

const ffprobeCommand = `"${ffprobePath}" -v error -show_streams -of json "${video}"`
const { stdout: probeOut } = await exec(ffprobeCommand)
const metadata = JSON.parse(probeOut)

const allStreams = metadata.streams || []
const videoStream = allStreams.find((s: any) => s.codec_type === 'video')
const hasAudio = allStreams.some((s: any) => s.codec_type === 'audio')
const hasSubtitle = allStreams.some((s: any) => s.codec_type === 'subtitle')

if (videoStream) {
const frameCount = videoStream.nb_frames || '未知'
const frameRate = videoStream.avg_frame_rate || '未知'
const resolution = `${videoStream.width}x${videoStream.height}`
const audioText = hasAudio ? '是' : '否'
const subtitleText = hasSubtitle ? '是' : '否'

event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `正在处理输入视频 ${video} 的信息:\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧数(输入): ${frameCount}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧率(输入): ${frameRate}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `分辨率(输入): ${resolution}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `是否含有音频: ${audioText}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `是否含有字幕: ${subtitleText}\n`)
}

return {
hasAudio,
hasSubtitle,
videoStream,
}
}

// 新增:获取输出视频信息的独立函数
async function getOutputVideoInfo(event: IpcMainEvent, vpyPath: string): Promise<{
width: string
height: string
frames: string
fps: string
}> {
const vspipePath = getExecPath().vspipe

const info: {
width: string
height: string
frames: string
fps: string
} = {
width: '未知',
height: '未知',
frames: '0',
fps: '0',
}

await new Promise<void>((resolve, reject) => {
const vspipeInfoProcess = spawn(vspipePath, ['--info', vpyPath])
addProcess('vspipe', vspipeInfoProcess)

let vspipeOut = '' // 用于保存 stdout 内容
// eslint-disable-next-line unused-imports/no-unused-vars
let stderrOut = '' // 用于保存 stderr 内容

vspipeInfoProcess.stdout.on('data', (data: Buffer) => {
const str = iconv.decode(data, 'gbk')
vspipeOut += str
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `${str}`)
})

vspipeInfoProcess.stderr.on('data', (data: Buffer) => {
const str = iconv.decode(data, 'gbk')
stderrOut += str
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `${str}`)
})
Comment on lines +165 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

变量 stderrOut 被赋值但从未被使用。这是一个无用的变量,建议移除以保持代码整洁。

    vspipeInfoProcess.stdout.on('data', (data: Buffer) => {
      const str = iconv.decode(data, 'gbk')
      vspipeOut += str
      event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `${str}`)
    })

    vspipeInfoProcess.stderr.on('data', (data: Buffer) => {
      const str = iconv.decode(data, 'gbk')
      event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `${str}`)
    })


vspipeInfoProcess.on('close', (code) => {
removeProcess(vspipeInfoProcess)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `vspipe info 执行完毕,退出码: ${code}\n`)

info.width = vspipeOut.match(/Width:\s*(\d+)/)?.[1] || '未知'
info.height = vspipeOut.match(/Height:\s*(\d+)/)?.[1] || '未知'
info.frames = vspipeOut.match(/Frames:\s*(\d+)/)?.[1] || '0'
info.fps = vspipeOut.match(/FPS:\s*([\d/]+)\s*\(([\d.]+) fps\)/)?.[2] || '0'

event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `======= 输出视频信息 =======\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `宽: ${info.width}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `高: ${info.height}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧数: ${info.frames}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧率: ${info.fps}\n`)
resolve()
})

vspipeInfoProcess.on('error', (err) => {
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `vspipe 执行出错: ${err.message}\n`)
reject(err)
})
})

return info
}

export async function runCommand(event: IpcMainEvent, taskConfig: TaskConfig): Promise<void> {
const vpyContent = taskConfig.vpyContent
const ffmpegCMD = taskConfig.ffmpegCMD

const vspipePath = getExecPath().vspipe
const ffmpegPath = getExecPath().ffmpeg
const ffprobePath = getExecPath().ffprobe

const videos = Array.isArray(taskConfig.fileList) ? taskConfig.fileList : []

Expand All @@ -62,99 +167,21 @@ export async function runCommand(event: IpcMainEvent, taskConfig: TaskConfig): P
}
try {
// ========== 1. 获取输入视频信息 ==========
const ffprobeCommand = `"${ffprobePath}" -v error -show_streams -of json "${video}"`
const { stdout: probeOut } = await exec(ffprobeCommand)
const metadata = JSON.parse(probeOut)

const allStreams = metadata.streams || []
const videoStream = allStreams.find((s: any) => s.codec_type === 'video')
const hasAudio = allStreams.some((s: any) => s.codec_type === 'audio')
const hasSubtitle = allStreams.some((s: any) => s.codec_type === 'subtitle')

if (videoStream) {
const frameCount = videoStream.nb_frames || '未知'
const frameRate = videoStream.avg_frame_rate || '未知'
const resolution = `${videoStream.width}x${videoStream.height}`
const audioText = hasAudio ? '是' : '否'
const subtitleText = hasSubtitle ? '是' : '否' // 字幕信息

event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `正在处理输入视频 ${video} 的信息:\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧数(输入): ${frameCount}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧率(输入): ${frameRate}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `分辨率(输入): ${resolution}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `是否含有音频: ${audioText}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `是否含有字幕: ${subtitleText}\n`)
}
const { hasAudio, hasSubtitle } = await getInputVideoInfo(event, video)

// ========== 2. 生成 vpy 文件 ==========
// 生成唯一 vpy 路径
const baseName = path.basename(video, path.extname(video))
const vpyPath = getGenVpyPath(taskConfig, baseName)
await writeVpyFile(null, vpyPath, vpyContent, video)

// ========== 3. 获取输出视频信息 ==========
let info: {
width: string
height: string
frames: string
fps: string
} = {
width: '未知',
height: '未知',
frames: '0',
fps: '0',
}
await new Promise<void>((resolve, reject) => {
const vspipeInfoProcess = spawn(vspipePath, ['--info', vpyPath])
addProcess('vspipe', vspipeInfoProcess)

let vspipeOut = '' // 用于保存 stdout 内容
// eslint-disable-next-line unused-imports/no-unused-vars
let stderrOut = '' // 用于保存 stderr 内容

vspipeInfoProcess.stdout.on('data', (data: Buffer) => {
const str = iconv.decode(data, 'gbk')
vspipeOut += str
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `${str}`)
})

vspipeInfoProcess.stderr.on('data', (data: Buffer) => {
const str = iconv.decode(data, 'gbk')
stderrOut += str
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `${str}`)
})

vspipeInfoProcess.on('close', (code) => {
removeProcess(vspipeInfoProcess)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `vspipe info 执行完毕,退出码: ${code}\n`)///////

info = {
width: vspipeOut.match(/Width:\s*(\d+)/)?.[1] || '未知',
height: vspipeOut.match(/Height:\s*(\d+)/)?.[1] || '未知',
frames: vspipeOut.match(/Frames:\s*(\d+)/)?.[1] || '0',
fps: vspipeOut.match(/FPS:\s*([\d/]+)\s*\(([\d.]+) fps\)/)?.[2] || '0',
}

event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `======= 输出视频信息 =======\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `宽: ${info.width}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `高: ${info.height}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧数: ${info.frames}\n`)
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `帧率: ${info.fps}\n`)
resolve()
})

vspipeInfoProcess.on('error', (err) => {
event.sender.send(IpcChannelOn.FFMPEG_OUTPUT, `vspipe 执行出错: ${err.message}\n`)
reject(err)
})
})
const info = await getOutputVideoInfo(event, vpyPath)

// ========== 4. 构建渲染命令 ==========
const vspipeArgs = ffmpegCMD[0].replace(MagicStr.VPY_PATH, vpyPath)
const ffmpegMajorArgs = ffmpegCMD[1]
const ffmpegMinorArgs = ffmpegCMD[2]
const ffmpeg_audio_sub_Args = generate_cmd(taskConfig, hasAudio, hasSubtitle)

const ffmpegArgs = ffmpegMajorArgs.replace(MagicStr.VIDEO_PATH, video) + ffmpeg_audio_sub_Args + ffmpegMinorArgs.replace(MagicStr.VIDEO_NAME, path.join(taskConfig.outputFolder, `${baseName}_enhance`) + taskConfig.videoContainer)

const full_cmd = `${`"${vspipePath}" ${vspipeArgs}`} | "${ffmpegPath}" ${ffmpegArgs}`
Expand Down
8 changes: 6 additions & 2 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge } from 'electron'
import { contextBridge, ipcRenderer } from 'electron'

// Custom APIs for renderer
const api = {}

contextBridge.exposeInMainWorld('electronWindow', {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
})
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
Expand Down
Loading