Files
QZMusic_PC/electron/main.ts
2026-01-21 15:39:22 +08:00

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()
})