- Клиентская часть Vue 3 + Vite - Серверная часть Node.js + WebSocket - Система авторизации и смен - Управление игровыми портами - Поддержка тем (светлая/темная) - Адаптивный дизайн 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
469 lines
19 KiB
JavaScript
469 lines
19 KiB
JavaScript
// Windows-специфичный аудио плеер с поддержкой изменения громкости в реальном времени
|
||
const { spawn, exec } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const net = require('net');
|
||
|
||
class WindowsAudioPlayer {
|
||
constructor() {
|
||
this.currentProcess = null;
|
||
this.currentFile = null;
|
||
this.isPlaying = false;
|
||
this.isPaused = false;
|
||
this.volume = 70;
|
||
this.position = 0;
|
||
this.server = null;
|
||
this.client = null;
|
||
this.playerReady = false;
|
||
|
||
console.log('[WINDOWS-AUDIO] Инициализирован Windows аудио плеер');
|
||
}
|
||
|
||
play(filePath, startPosition = 0) {
|
||
this.stop();
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
console.error('[WINDOWS-AUDIO] Файл не найден:', filePath);
|
||
return this;
|
||
}
|
||
|
||
this.currentFile = filePath;
|
||
this.position = startPosition;
|
||
|
||
// Создаем PowerShell скрипт с поддержкой управления громкостью
|
||
const volumeValue = this.volume / 100;
|
||
console.log('[WINDOWS-AUDIO] Установка громкости MediaPlayer:', volumeValue, '(', this.volume, '%)');
|
||
|
||
const script = `
|
||
Add-Type -AssemblyName presentationcore
|
||
$player = New-Object System.Windows.Media.MediaPlayer
|
||
$player.Volume = ${volumeValue}
|
||
$player.Open("${filePath}")
|
||
Write-Host "LOADING_FILE:${filePath}"
|
||
|
||
# Ждем загрузки
|
||
$timeout = 0
|
||
while ($player.NaturalDuration.HasTimeSpan -eq $false -and $timeout -lt 50) {
|
||
Start-Sleep -Milliseconds 100
|
||
$timeout++
|
||
}
|
||
|
||
# Проверяем, что файл загрузился
|
||
if ($player.NaturalDuration.HasTimeSpan -eq $false) {
|
||
Write-Host "ERROR: Failed to load file"
|
||
exit 1
|
||
}
|
||
|
||
# Устанавливаем позицию если нужно
|
||
if (${startPosition} -gt 0) {
|
||
$player.Position = [TimeSpan]::FromSeconds(${startPosition})
|
||
}
|
||
|
||
# Запускаем воспроизведение
|
||
$player.Play()
|
||
Write-Host "PLAYING"
|
||
|
||
# Главный цикл с проверкой команд
|
||
$commandFile = "$env:TEMP\\audio_player_$PID.cmd"
|
||
$volumeFile = "$env:TEMP\\audio_player_$PID.vol"
|
||
$counter = 0
|
||
|
||
while ($true) {
|
||
# Периодически логируем текущую громкость (каждые 2 секунды)
|
||
$counter++
|
||
if ($counter % 40 -eq 0) {
|
||
Write-Host "CURRENT_VOLUME:$([int]($player.Volume * 100))"
|
||
$counter = 0
|
||
}
|
||
# Проверяем команду
|
||
if (Test-Path $commandFile) {
|
||
$command = Get-Content $commandFile
|
||
Remove-Item $commandFile -Force
|
||
|
||
switch ($command) {
|
||
"STOP" {
|
||
$player.Stop()
|
||
$player.Close()
|
||
exit
|
||
}
|
||
"PAUSE" {
|
||
$player.Pause()
|
||
}
|
||
"RESUME" {
|
||
$player.Play()
|
||
}
|
||
}
|
||
}
|
||
|
||
# Проверяем изменение громкости
|
||
if (Test-Path $volumeFile) {
|
||
$newVolume = Get-Content $volumeFile
|
||
Remove-Item $volumeFile -Force
|
||
$oldVolume = $player.Volume
|
||
$volumeValue = [double]$newVolume / 100
|
||
$player.Volume = $volumeValue
|
||
Write-Host "VOLUME_CHANGED:$newVolume"
|
||
Write-Host "PLAYER_VOLUME_BEFORE:$([int]($oldVolume * 100))"
|
||
Write-Host "PLAYER_VOLUME_AFTER:$([int]($player.Volume * 100))"
|
||
Write-Host "VOLUME_SET_SUCCESS:$($player.Volume -eq $volumeValue)"
|
||
}
|
||
|
||
# Проверяем завершение воспроизведения
|
||
if ($player.NaturalDuration.HasTimeSpan -and $player.Position -ge $player.NaturalDuration.TimeSpan) {
|
||
Write-Host "FINISHED"
|
||
break
|
||
}
|
||
|
||
|
||
Start-Sleep -Milliseconds 50
|
||
}
|
||
|
||
$player.Close()`;
|
||
|
||
console.log('[WINDOWS-AUDIO] Запускаем воспроизведение:', filePath);
|
||
|
||
this.currentProcess = spawn('powershell', [
|
||
'-WindowStyle', 'Hidden',
|
||
'-Command', script
|
||
], {
|
||
windowsHide: true
|
||
});
|
||
|
||
this.isPlaying = true;
|
||
this.isPaused = false;
|
||
|
||
// Сохраняем PID процесса для использования в setVolume
|
||
console.log('[WINDOWS-AUDIO] Процесс запущен с PID:', this.currentProcess.pid);
|
||
|
||
// Обработка вывода
|
||
this.currentProcess.stdout.on('data', (data) => {
|
||
const output = data.toString().trim();
|
||
console.log('[WINDOWS-AUDIO] Вывод:', output);
|
||
|
||
if (output === 'PLAYING') {
|
||
this.playerReady = true;
|
||
} else if (output === 'FINISHED') {
|
||
this.isPlaying = false;
|
||
} else if (output.startsWith('VOLUME_CHANGED:')) {
|
||
console.log('[WINDOWS-AUDIO] ✅ Громкость успешно изменена:', output);
|
||
} else if (output.startsWith('PLAYER_VOLUME:')) {
|
||
console.log('[WINDOWS-AUDIO] 🔊 Текущая громкость плеера:', output);
|
||
} else if (output.startsWith('PLAYER_VOLUME_BEFORE:')) {
|
||
console.log('[WINDOWS-AUDIO] 📊 Громкость ДО изменения:', output);
|
||
} else if (output.startsWith('PLAYER_VOLUME_AFTER:')) {
|
||
console.log('[WINDOWS-AUDIO] 📊 Громкость ПОСЛЕ изменения:', output);
|
||
} else if (output.startsWith('VOLUME_SET_SUCCESS:')) {
|
||
console.log('[WINDOWS-AUDIO] ✔️ Успешность изменения громкости:', output);
|
||
} else if (output.startsWith('CURRENT_VOLUME:')) {
|
||
// Периодическое логирование текущей громкости
|
||
console.log('[WINDOWS-AUDIO] 🎚️ Текущая громкость MediaPlayer:', output);
|
||
} else if (output.startsWith('WATCHING_FILES:')) {
|
||
// Логирование файлов для отладки
|
||
} else if (output.startsWith('ALIVE:')) {
|
||
// Периодическая проверка состояния
|
||
} else if (output.startsWith('VOLUME_SET:')) {
|
||
console.log('[WINDOWS-AUDIO] Установлена громкость MediaPlayer:', output);
|
||
} else if (output.startsWith('ERROR:')) {
|
||
console.error('[WINDOWS-AUDIO] ОШИБКА:', output);
|
||
this.isPlaying = false;
|
||
} else if (output.startsWith('STATUS:')) {
|
||
console.log('[WINDOWS-AUDIO] Статус воспроизведения:', output);
|
||
} else if (output.startsWith('LOADING_FILE:')) {
|
||
console.log('[WINDOWS-AUDIO] Загрузка файла:', output);
|
||
} else if (output.startsWith('FILE_LOADED:')) {
|
||
console.log('[WINDOWS-AUDIO] Файл загружен:', output);
|
||
}
|
||
});
|
||
|
||
// Обработка ошибок
|
||
this.currentProcess.stderr.on('data', (data) => {
|
||
console.error('[WINDOWS-AUDIO] Ошибка PowerShell:', data.toString());
|
||
});
|
||
|
||
this.currentProcess.on('close', (code) => {
|
||
console.log('[WINDOWS-AUDIO] Процесс завершен:', code);
|
||
this.isPlaying = false;
|
||
this.isPaused = false;
|
||
this.currentProcess = null;
|
||
this.playerReady = false;
|
||
this._cleanupTempFiles();
|
||
});
|
||
|
||
this.currentProcess.on('error', (err) => {
|
||
console.error('[WINDOWS-AUDIO] Ошибка процесса:', err.message);
|
||
this.isPlaying = false;
|
||
this.currentProcess = null;
|
||
this._cleanupTempFiles();
|
||
});
|
||
|
||
return this;
|
||
}
|
||
|
||
stop() {
|
||
if (this.currentProcess) {
|
||
console.log('[WINDOWS-AUDIO] Остановка процесса PID:', this.currentProcess.pid);
|
||
|
||
// Сразу убиваем процесс PowerShell
|
||
try {
|
||
process.kill(this.currentProcess.pid, 'SIGTERM');
|
||
} catch (e) {
|
||
console.log('[WINDOWS-AUDIO] Не удалось остановить через SIGTERM, используем taskkill');
|
||
try {
|
||
spawn('taskkill', ['/F', '/T', '/PID', this.currentProcess.pid.toString()], {
|
||
stdio: 'ignore',
|
||
windowsHide: true
|
||
});
|
||
} catch (e2) {
|
||
console.error('[WINDOWS-AUDIO] Ошибка при остановке процесса:', e2);
|
||
}
|
||
}
|
||
|
||
this.currentProcess = null;
|
||
}
|
||
|
||
this.isPlaying = false;
|
||
this.isPaused = false;
|
||
this.playerReady = false;
|
||
this._cleanupTempFiles();
|
||
|
||
return this;
|
||
}
|
||
|
||
pause() {
|
||
if (!this.isPlaying || this.isPaused || !this.currentProcess) return this;
|
||
|
||
console.log('[WINDOWS-AUDIO] Пауза не поддерживается, останавливаем воспроизведение');
|
||
// PowerShell MediaPlayer не поддерживает паузу надёжно, поэтому останавливаем
|
||
this.stop();
|
||
|
||
return this;
|
||
}
|
||
|
||
resume() {
|
||
console.log('[WINDOWS-AUDIO] Возобновление не поддерживается');
|
||
return this;
|
||
}
|
||
|
||
setVolume(volume) {
|
||
this.volume = Math.max(0, Math.min(100, volume));
|
||
console.log('[WINDOWS-AUDIO] Установка громкости:', this.volume);
|
||
|
||
// Если играет, применяем громкость в реальном времени
|
||
if (this.isPlaying && this.currentProcess) {
|
||
const volumeFile = `${process.env.TEMP}\\audio_player_${this.currentProcess.pid}.vol`;
|
||
try {
|
||
// Записываем файл несколько раз для надежности
|
||
fs.writeFileSync(volumeFile, this.volume.toString());
|
||
console.log('[WINDOWS-AUDIO] Команда изменения громкости отправлена в файл:', volumeFile);
|
||
|
||
// Дублируем запись через 100мс для надежности
|
||
setTimeout(() => {
|
||
if (this.currentProcess && this.isPlaying) {
|
||
try {
|
||
fs.writeFileSync(volumeFile, this.volume.toString());
|
||
console.log('[WINDOWS-AUDIO] Повторная отправка команды громкости');
|
||
} catch (e) {
|
||
// Игнорируем ошибку
|
||
}
|
||
}
|
||
}, 100);
|
||
} catch (e) {
|
||
console.error('[WINDOWS-AUDIO] Ошибка при изменении громкости:', e.message);
|
||
// НЕ меняем системную громкость - это влияет на всё
|
||
console.log('[WINDOWS-AUDIO] Громкость будет применена при следующем воспроизведении');
|
||
}
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
_setSystemVolume() {
|
||
const volumePercent = this.volume / 100;
|
||
const volumeScript = `
|
||
Add-Type -TypeDefinition @'
|
||
using System.Runtime.InteropServices;
|
||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
interface IAudioEndpointVolume {
|
||
int f(); int g(); int h(); int i();
|
||
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
|
||
}
|
||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
interface IMMDevice {
|
||
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
|
||
}
|
||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
interface IMMDeviceEnumerator {
|
||
int f();
|
||
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
|
||
}
|
||
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
|
||
public class Audio {
|
||
static IAudioEndpointVolume Vol() {
|
||
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
|
||
IMMDevice dev = null;
|
||
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(0, 1, out dev));
|
||
IAudioEndpointVolume epv = null;
|
||
var epvid = typeof(IAudioEndpointVolume).GUID;
|
||
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, 23, 0, out epv));
|
||
return epv;
|
||
}
|
||
public static void SetVolume(float level) {
|
||
Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(level, System.Guid.Empty));
|
||
}
|
||
}
|
||
'@
|
||
[Audio]::SetVolume(${volumePercent})`;
|
||
|
||
exec(`powershell -WindowStyle Hidden -Command "${volumeScript}"`, (err) => {
|
||
if (!err) {
|
||
console.log('[WINDOWS-AUDIO] Системная громкость установлена:', this.volume + '%');
|
||
}
|
||
});
|
||
}
|
||
|
||
_cleanupTempFiles() {
|
||
// Очищаем временные файлы
|
||
const tempDir = process.env.TEMP;
|
||
if (tempDir) {
|
||
fs.readdir(tempDir, (err, files) => {
|
||
if (!err) {
|
||
files.forEach(file => {
|
||
if (file.startsWith('audio_player_') && (file.endsWith('.cmd') || file.endsWith('.vol'))) {
|
||
fs.unlink(path.join(tempDir, file), () => {});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
getPosition() {
|
||
if (!this.isPlaying) return 0;
|
||
// Примерная позиция - для точной нужно было бы запрашивать у плеера
|
||
return this.position;
|
||
}
|
||
}
|
||
|
||
// Радио плеер для Windows
|
||
class WindowsRadioPlayer extends WindowsAudioPlayer {
|
||
playStream(url) {
|
||
this.stop();
|
||
|
||
console.log('[WINDOWS-RADIO] Воспроизведение потока:', url);
|
||
|
||
const volumeValue = Math.max(0.2, this.volume / 100);
|
||
console.log('[WINDOWS-RADIO] Установка громкости MediaPlayer:', volumeValue, '(', this.volume, '%)');
|
||
|
||
const script = `
|
||
Add-Type -AssemblyName presentationcore
|
||
$player = New-Object System.Windows.Media.MediaPlayer
|
||
$player.Volume = ${volumeValue}
|
||
$player.Open("${url}")
|
||
Write-Host "LOADING_RADIO:${url}"
|
||
|
||
# Ждем буферизации
|
||
Start-Sleep -Seconds 2
|
||
|
||
$player.Play()
|
||
Write-Host "PLAYING"
|
||
|
||
# Главный цикл с проверкой команд
|
||
$commandFile = "$env:TEMP\\audio_player_$PID.cmd"
|
||
$volumeFile = "$env:TEMP\\audio_player_$PID.vol"
|
||
$counter = 0
|
||
|
||
while ($true) {
|
||
# Периодически логируем текущую громкость (каждые 2 секунды)
|
||
$counter++
|
||
if ($counter % 40 -eq 0) {
|
||
Write-Host "CURRENT_VOLUME:$([int]($player.Volume * 100))"
|
||
$counter = 0
|
||
}
|
||
# Проверяем команду
|
||
if (Test-Path $commandFile) {
|
||
$command = Get-Content $commandFile
|
||
Remove-Item $commandFile -Force
|
||
|
||
if ($command -eq "STOP") {
|
||
$player.Stop()
|
||
$player.Close()
|
||
exit
|
||
}
|
||
}
|
||
|
||
# Проверяем изменение громкости
|
||
if (Test-Path $volumeFile) {
|
||
$newVolume = Get-Content $volumeFile
|
||
Remove-Item $volumeFile -Force
|
||
$oldVolume = $player.Volume
|
||
$volumeValue = [double]$newVolume / 100
|
||
$player.Volume = $volumeValue
|
||
Write-Host "VOLUME_CHANGED:$newVolume"
|
||
Write-Host "PLAYER_VOLUME_BEFORE:$([int]($oldVolume * 100))"
|
||
Write-Host "PLAYER_VOLUME_AFTER:$([int]($player.Volume * 100))"
|
||
Write-Host "VOLUME_SET_SUCCESS:$($player.Volume -eq $volumeValue)"
|
||
}
|
||
|
||
Start-Sleep -Milliseconds 50
|
||
}`;
|
||
|
||
this.currentProcess = spawn('powershell', [
|
||
'-WindowStyle', 'Hidden',
|
||
'-Command', script
|
||
], {
|
||
windowsHide: true
|
||
});
|
||
|
||
this.isPlaying = true;
|
||
this.isPaused = false;
|
||
|
||
// Обработка вывода
|
||
this.currentProcess.stdout.on('data', (data) => {
|
||
const output = data.toString().trim();
|
||
console.log('[WINDOWS-RADIO] Вывод:', output);
|
||
|
||
if (output === 'PLAYING') {
|
||
this.playerReady = true;
|
||
} else if (output.startsWith('VOLUME_CHANGED:')) {
|
||
console.log('[WINDOWS-RADIO] ✅ Громкость успешно изменена:', output);
|
||
} else if (output.startsWith('PLAYER_VOLUME_BEFORE:')) {
|
||
console.log('[WINDOWS-RADIO] 📊 Громкость ДО изменения:', output);
|
||
} else if (output.startsWith('PLAYER_VOLUME_AFTER:')) {
|
||
console.log('[WINDOWS-RADIO] 📊 Громкость ПОСЛЕ изменения:', output);
|
||
} else if (output.startsWith('VOLUME_SET_SUCCESS:')) {
|
||
console.log('[WINDOWS-RADIO] ✔️ Успешность изменения громкости:', output);
|
||
} else if (output.startsWith('CURRENT_VOLUME:')) {
|
||
console.log('[WINDOWS-RADIO] 🎚️ Текущая громкость:', output);
|
||
} else if (output.startsWith('LOADING_RADIO:')) {
|
||
console.log('[WINDOWS-RADIO] Загрузка радио:', output);
|
||
} else if (output.startsWith('ERROR:')) {
|
||
console.error('[WINDOWS-RADIO] ОШИБКА:', output);
|
||
this.isPlaying = false;
|
||
}
|
||
});
|
||
|
||
// Обработка ошибок
|
||
this.currentProcess.stderr.on('data', (data) => {
|
||
console.error('[WINDOWS-RADIO] Ошибка PowerShell:', data.toString());
|
||
});
|
||
|
||
this.currentProcess.on('close', (code) => {
|
||
console.log('[WINDOWS-RADIO] Процесс завершен:', code);
|
||
this.isPlaying = false;
|
||
this.currentProcess = null;
|
||
this.playerReady = false;
|
||
this._cleanupTempFiles();
|
||
});
|
||
|
||
this.currentProcess.on('error', (err) => {
|
||
console.error('[WINDOWS-RADIO] Ошибка процесса:', err.message);
|
||
this.isPlaying = false;
|
||
this.currentProcess = null;
|
||
this._cleanupTempFiles();
|
||
});
|
||
|
||
return this;
|
||
}
|
||
}
|
||
|
||
module.exports = { WindowsAudioPlayer, WindowsRadioPlayer }; |