mirror of
https://github.com/lqtmcstudio/QZMusic_PC.git
synced 2026-06-20 23:35:06 +08:00
237 lines
7.3 KiB
TypeScript
237 lines
7.3 KiB
TypeScript
import { app, BrowserWindow, Menu, ipcMain } from 'electron'
|
|
import { createRequire } from 'node:module'
|
|
import { fileURLToPath } from 'node:url'
|
|
import path from 'node:path'
|
|
import os from 'node:os'
|
|
import net from 'node:net'
|
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
process.env.APP_ROOT = path.join(__dirname, '..')
|
|
|
|
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
|
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
|
|
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
|
|
|
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
|
|
|
|
let win: BrowserWindow | null
|
|
|
|
|
|
class MpvController {
|
|
private process: ChildProcess | null = null;
|
|
private socket: net.Socket | null = null;
|
|
private socketPath: string;
|
|
private buffer: string = '';
|
|
|
|
constructor() {
|
|
const socketName = `mpv-socket-${Date.now()}`;
|
|
this.socketPath = process.platform === 'win32'
|
|
? `\\\\.\\pipe\\${socketName}`
|
|
: path.join(os.tmpdir(), socketName);
|
|
}
|
|
|
|
start(binaryPath: string) {
|
|
console.log(`[MPV] Starting from: ${binaryPath}`);
|
|
console.log(`[MPV] IPC Socket: ${this.socketPath}`);
|
|
|
|
// 启动 MPV 进程
|
|
this.process = spawn(binaryPath, [
|
|
'--idle', // 空闲时不退出
|
|
'--no-video', // 纯音频模式
|
|
'--keep-open=yes', // 播放结束不退出
|
|
`--input-ipc-server=${this.socketPath}`
|
|
]);
|
|
|
|
this.process.on('error', (err) => {
|
|
console.error('[MPV] Process Error:', err);
|
|
});
|
|
|
|
this.process.on('exit', (code) => {
|
|
console.log(`[MPV] Process exited with code ${code}`);
|
|
this.socket?.destroy();
|
|
});
|
|
|
|
this.connectSocket();
|
|
}
|
|
|
|
private connectSocket(retries = 10) {
|
|
setTimeout(() => {
|
|
const socket = net.createConnection(this.socketPath);
|
|
|
|
socket.on('connect', () => {
|
|
console.log('[MPV] IPC Connected!');
|
|
this.socket = socket;
|
|
this.setupObservers();
|
|
});
|
|
|
|
socket.on('data', (data) => this.handleData(data));
|
|
|
|
socket.on('error', (err) => {
|
|
if (retries > 0) {
|
|
// MPV 还没准备好,继续重试
|
|
this.connectSocket(retries - 1);
|
|
} else {
|
|
console.error('[MPV] Failed to connect to IPC socket:', err);
|
|
}
|
|
});
|
|
}, 500); // 500ms 重试间隔
|
|
}
|
|
|
|
// 处理接收到的数据
|
|
private handleData(data: Buffer) {
|
|
this.buffer += data.toString();
|
|
|
|
// MPV 的消息以 \n 分隔
|
|
const lines = this.buffer.split('\n');
|
|
// 最后一个元素可能是不完整的行,留到下一次处理
|
|
this.buffer = lines.pop() || '';
|
|
|
|
lines.forEach(line => {
|
|
if (!line.trim()) return;
|
|
try {
|
|
const message = JSON.parse(line);
|
|
console.log(message);
|
|
this.handleMessage(message);
|
|
} catch (e) {
|
|
console.error('[MPV] JSON Parse Error:', e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 处理解析后的 JSON 消息
|
|
private handleMessage(msg: any) {
|
|
if (!win) return;
|
|
|
|
// 处理属性变更事件
|
|
if (msg.event === 'property-change') {
|
|
switch (msg.name) {
|
|
case 'time-pos':
|
|
win.webContents.send('mpv-time-update', msg.data);
|
|
break;
|
|
case 'duration':
|
|
win.webContents.send('mpv-duration', msg.data);
|
|
break;
|
|
case 'pause':
|
|
win.webContents.send('mpv-play-state', !msg.data); // data=true 意味着暂停
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 处理播放结束事件
|
|
if (msg.event === 'end-file') {
|
|
win.webContents.send('mpv-ended');
|
|
win.webContents.send('mpv-play-state', false);
|
|
}
|
|
}
|
|
|
|
// 初始化监听属性
|
|
private setupObservers() {
|
|
this.send(['observe_property', 1, 'time-pos']);
|
|
this.send(['observe_property', 2, 'duration']);
|
|
this.send(['observe_property', 3, 'pause']);
|
|
}
|
|
|
|
// 发送命令给 MPV
|
|
send(command: any[]) {
|
|
if (!this.socket || this.socket.destroyed) return;
|
|
|
|
const payload = JSON.stringify({ command });
|
|
this.socket.write(payload + '\n');
|
|
}
|
|
|
|
// === 业务方法 ===
|
|
|
|
load(url: string, autoPlay: boolean) {
|
|
if (autoPlay) {
|
|
// 确保取消暂停 (防止之前是暂停状态)
|
|
this.send(['set_property', 'pause', false]);
|
|
// 加载文件
|
|
this.send(['loadfile', url, 'replace']);
|
|
} else {
|
|
// 先设置为暂停 (这样后续加载的文件会继承这个暂停状态)
|
|
this.send(['set_property', 'pause', true]);
|
|
// 加载文件
|
|
this.send(['loadfile', url, 'replace']);
|
|
}
|
|
}
|
|
|
|
play() { this.send(['set_property', 'pause', false]); }
|
|
|
|
pause() { this.send(['set_property', 'pause', true]); }
|
|
|
|
seek(time: number) { this.send(['seek', time, 'absolute']); }
|
|
|
|
setVolume(volume: number) { this.send(['set_property', 'volume', volume]); }
|
|
}
|
|
|
|
const mpv = new MpvController();
|
|
|
|
// === Electron 窗口逻辑 ===
|
|
|
|
const mpvExecutablePath = app.isPackaged
|
|
? path.join(process.resourcesPath, 'core', 'mpv.exe')
|
|
: path.join(process.env.APP_ROOT, 'core', 'mpv.exe');
|
|
|
|
function createWindow() {
|
|
win = new BrowserWindow({
|
|
frame: false,
|
|
minWidth: 950,
|
|
minHeight: 700,
|
|
width: 1000,
|
|
height: 800,
|
|
icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.mjs'),
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
},
|
|
})
|
|
|
|
win.webContents.on('did-finish-load', () => {
|
|
win?.webContents.send('main-process-message', new Date().toLocaleString())
|
|
})
|
|
|
|
if (VITE_DEV_SERVER_URL) {
|
|
win.loadURL(VITE_DEV_SERVER_URL)
|
|
} else {
|
|
win.loadFile(path.join(RENDERER_DIST, 'index.html'))
|
|
}
|
|
}
|
|
|
|
// === IPC 监听 ===
|
|
|
|
ipcMain.on('window-minimize', (event) => BrowserWindow.fromWebContents(event.sender)?.minimize())
|
|
ipcMain.on('window-maximize', () => win?.isMaximized() ? win.unmaximize() : win?.maximize())
|
|
ipcMain.on('window-close', () => win?.close())
|
|
ipcMain.handle('window-is-maximized', () => win?.isMaximized() || false)
|
|
|
|
// MPV 控制指令
|
|
ipcMain.on('mpv-load', (_, url, autoPlay = true) => mpv.load(url, autoPlay))
|
|
ipcMain.on('mpv-play', () => mpv.play())
|
|
ipcMain.on('mpv-pause', () => mpv.pause())
|
|
ipcMain.on('mpv-seek', (_, time) => mpv.seek(time))
|
|
ipcMain.on('mpv-volume', (_, volume) => mpv.setVolume(volume))
|
|
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit()
|
|
win = null
|
|
}
|
|
})
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow()
|
|
}
|
|
})
|
|
|
|
app.whenReady().then(() => {
|
|
Menu.setApplicationMenu(null)
|
|
// 启动 MPV
|
|
mpv.start(mpvExecutablePath);
|
|
createWindow()
|
|
}) |