225 lines
8.3 KiB
Python
225 lines
8.3 KiB
Python
"""VLC service for playing sounds using subprocess."""
|
|
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
from typing import Dict, List, Optional
|
|
|
|
from app.database import db
|
|
from app.models.sound import Sound
|
|
|
|
|
|
class VLCService:
|
|
"""Service for playing sounds using VLC subprocess."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize VLC service."""
|
|
self.processes: Dict[str, subprocess.Popen] = {}
|
|
self.lock = threading.Lock()
|
|
|
|
def play_sound(self, sound_id: int) -> bool:
|
|
"""Play a sound by ID using VLC subprocess."""
|
|
try:
|
|
# Get sound from database
|
|
sound = Sound.query.get(sound_id)
|
|
if not sound:
|
|
return False
|
|
|
|
# Use normalized file if available, otherwise use original
|
|
if sound.is_normalized and sound.normalized_filename:
|
|
sound_path = os.path.join(
|
|
"sounds",
|
|
"normalized",
|
|
"soundboard",
|
|
sound.normalized_filename,
|
|
)
|
|
else:
|
|
sound_path = os.path.join(
|
|
"sounds", "soundboard", sound.filename
|
|
)
|
|
|
|
# Check if file exists
|
|
if not os.path.exists(sound_path):
|
|
return False
|
|
|
|
# Convert to absolute path
|
|
sound_path = os.path.abspath(sound_path)
|
|
|
|
# Create unique process ID
|
|
process_id = f"sound_{sound_id}_{int(time.time() * 1000000)}"
|
|
|
|
# Start VLC process
|
|
vlc_cmd = [
|
|
"vlc",
|
|
sound_path,
|
|
"--intf",
|
|
"dummy", # No interface
|
|
"--play-and-exit", # Exit after playing
|
|
"--no-video", # Audio only
|
|
"--quiet", # Reduce output
|
|
]
|
|
|
|
process = subprocess.Popen(
|
|
vlc_cmd,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
preexec_fn=os.setsid, # Create new process group
|
|
)
|
|
|
|
# Store process for tracking
|
|
with self.lock:
|
|
self.processes[process_id] = process
|
|
|
|
print(
|
|
f"Started VLC process {process.pid} ({process_id}) for sound {sound.name}. Total processes: {len(self.processes)}"
|
|
)
|
|
|
|
# Increment play count
|
|
sound.increment_play_count()
|
|
|
|
# Schedule cleanup after sound duration
|
|
threading.Thread(
|
|
target=self._cleanup_after_playback,
|
|
args=(process_id, sound.duration if sound.duration else 10000),
|
|
daemon=True,
|
|
).start()
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Error starting VLC process for sound {sound_id}: {e}")
|
|
return False
|
|
|
|
def _cleanup_after_playback(self, process_id: str, duration: int) -> None:
|
|
"""Clean up VLC process after playback."""
|
|
# Wait for playback to finish (duration + 1 second buffer)
|
|
time.sleep(duration / 1000 + 1) # Convert ms to seconds
|
|
|
|
with self.lock:
|
|
if process_id in self.processes:
|
|
print(f"Cleaning up process {process_id} after playback")
|
|
process = self.processes[process_id]
|
|
|
|
try:
|
|
# Check if process is still running
|
|
if process.poll() is None:
|
|
print(
|
|
f"Process {process.pid} still running, terminating"
|
|
)
|
|
process.terminate()
|
|
# Give it a moment to terminate gracefully
|
|
try:
|
|
process.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
print(
|
|
f"Process {process.pid} didn't terminate, killing"
|
|
)
|
|
process.kill()
|
|
|
|
print(f"Successfully cleaned up process {process_id}")
|
|
except Exception as e:
|
|
print(f"Error during cleanup of {process_id}: {e}")
|
|
finally:
|
|
# Always remove from tracking
|
|
del self.processes[process_id]
|
|
print(
|
|
f"Removed process {process_id}. Remaining processes: {len(self.processes)}"
|
|
)
|
|
else:
|
|
print(f"Process {process_id} not found during cleanup")
|
|
|
|
def stop_all(self) -> None:
|
|
"""Stop all playing sounds by killing VLC processes."""
|
|
with self.lock:
|
|
processes_copy = dict(self.processes)
|
|
print(
|
|
f"Stopping {len(processes_copy)} VLC processes: {list(processes_copy.keys())}"
|
|
)
|
|
|
|
for process_id, process in processes_copy.items():
|
|
try:
|
|
if process.poll() is None: # Process is still running
|
|
print(
|
|
f"Terminating process {process.pid} ({process_id})"
|
|
)
|
|
process.terminate()
|
|
|
|
# Give it a moment to terminate gracefully
|
|
try:
|
|
process.wait(timeout=1)
|
|
print(
|
|
f"Process {process.pid} terminated gracefully"
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
print(
|
|
f"Process {process.pid} didn't terminate, killing forcefully"
|
|
)
|
|
process.kill()
|
|
process.wait() # Wait for it to be killed
|
|
else:
|
|
print(
|
|
f"Process {process.pid} ({process_id}) already finished"
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"Error stopping process {process_id}: {e}")
|
|
|
|
# Clear all processes
|
|
self.processes.clear()
|
|
print(f"Cleared all processes. Remaining: {len(self.processes)}")
|
|
|
|
def get_playing_count(self) -> int:
|
|
"""Get number of currently playing sounds."""
|
|
with self.lock:
|
|
# Clean up finished processes and return count
|
|
finished_processes = []
|
|
for process_id, process in self.processes.items():
|
|
if process.poll() is not None: # Process has finished
|
|
finished_processes.append(process_id)
|
|
|
|
# Remove finished processes
|
|
for process_id in finished_processes:
|
|
del self.processes[process_id]
|
|
|
|
return len(self.processes)
|
|
|
|
def force_stop_all(self) -> int:
|
|
"""Force stop all sounds by killing VLC processes aggressively."""
|
|
with self.lock:
|
|
stopped_count = len(self.processes)
|
|
print(f"Force stopping {stopped_count} VLC processes")
|
|
|
|
# # Kill all VLC processes aggressively
|
|
# for process_id, process in list(self.processes.items()):
|
|
# try:
|
|
# if process.poll() is None: # Process is still running
|
|
# print(f"Force killing process {process.pid} ({process_id})")
|
|
# process.kill()
|
|
# process.wait() # Wait for it to be killed
|
|
# print(f"Process {process.pid} killed")
|
|
# else:
|
|
# print(f"Process {process.pid} ({process_id}) already finished")
|
|
|
|
# except Exception as e:
|
|
# print(f"Error force-stopping process {process_id}: {e}")
|
|
|
|
# Also try to kill any remaining VLC processes system-wide
|
|
try:
|
|
subprocess.run(["pkill", "-f", "vlc"], check=False)
|
|
print("Killed any remaining VLC processes system-wide")
|
|
except Exception as e:
|
|
print(f"Error killing system VLC processes: {e}")
|
|
|
|
# Clear all processes
|
|
self.processes.clear()
|
|
print(
|
|
f"Force stop completed. Processes remaining: {len(self.processes)}"
|
|
)
|
|
return stopped_count
|
|
|
|
|
|
# Global VLC service instance
|
|
vlc_service = VLCService()
|