Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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()
}
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
}> {
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 +109 to +123

Choose a reason for hiding this comment

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

medium

变量 stderrOut 已声明但其值从未使用。为了代码整洁,建议移除此变量及其相关的所有赋值操作。

    let vspipeOut = '' // 用于保存 stdout 内容

    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