// 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 };