diff --git a/app/routes/soundboard.py b/app/routes/soundboard.py index a80e56e..7a22fcc 100644 --- a/app/routes/soundboard.py +++ b/app/routes/soundboard.py @@ -100,21 +100,21 @@ def get_status(): try: playing_count = vlc_service.get_playing_count() - # Get detailed instance information + # Get detailed process information with vlc_service.lock: - instances = [] - for instance_id, instance_data in vlc_service.instances.items(): - instances.append({ - "id": instance_id, - "sound_id": instance_data.get("sound_id"), - "created_at": instance_data.get("created_at"), + processes = [] + for process_id, process in vlc_service.processes.items(): + processes.append({ + "id": process_id, + "pid": process.pid, + "running": process.poll() is None, }) return jsonify( { "playing_count": playing_count, "is_playing": playing_count > 0, - "instances": instances, + "processes": processes, } ) except Exception as e: diff --git a/app/services/vlc_service.py b/app/services/vlc_service.py index 9fa0454..0df64d3 100644 --- a/app/services/vlc_service.py +++ b/app/services/vlc_service.py @@ -1,28 +1,27 @@ -"""VLC service for playing sounds.""" +"""VLC service for playing sounds using subprocess.""" import os +import signal +import subprocess import threading import time -import uuid -from typing import Any, Dict, Optional - -import vlc +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.""" + """Service for playing sounds using VLC subprocess.""" def __init__(self) -> None: """Initialize VLC service.""" - self.instances: Dict[str, Dict[str, Any]] = {} + 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.""" - with self.lock: + """Play a sound by ID using VLC subprocess.""" + try: # Get sound from database sound = Sound.query.get(sound_id) if not sound: @@ -34,7 +33,6 @@ class VLCService: "sounds", "normalized", "soundboard", - # sound.type.lower(), sound.normalized_filename, ) else: @@ -46,147 +44,179 @@ class VLCService: if not os.path.exists(sound_path): return False - # Create VLC instance - instance = vlc.Instance() - player = instance.media_player_new() + # Convert to absolute path + sound_path = os.path.abspath(sound_path) - # Load and play media - media = instance.media_new(sound_path) - player.set_media(media) + # Create unique process ID + process_id = f"sound_{sound_id}_{int(time.time() * 1000000)}" - # Start playback - player.play() + # 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 + ] - # Store instance for cleanup with unique ID - instance_id = f"sound_{sound_id}_{uuid.uuid4().hex[:8]}_{int(time.time())}" - self.instances[instance_id] = { - "instance": instance, - "player": player, - "sound_id": sound_id, - "created_at": time.time(), - } + process = subprocess.Popen( + vlc_cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + preexec_fn=os.setsid, # Create new process group + ) - print(f"Created instance {instance_id} for sound {sound.name}. Total instances: {len(self.instances)}") + # 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 + # Schedule cleanup after sound duration threading.Thread( target=self._cleanup_after_playback, - args=(instance_id, sound.duration if sound.duration else 10), + args=(process_id, sound.duration if sound.duration else 10000), daemon=True, ).start() return True - def _cleanup_after_playback(self, instance_id: str, duration: int) -> None: - """Clean up VLC instance after playback.""" + 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 instance_id in self.instances: - print(f"Cleaning up instance {instance_id} after playback") - instance_data = self.instances[instance_id] - player = instance_data["player"] - instance = instance_data["instance"] + if process_id in self.processes: + print(f"Cleaning up process {process_id} after playback") + process = self.processes[process_id] try: - # Stop player if still playing - if player.is_playing(): - player.stop() - - # Release resources - player.release() - instance.release() - print(f"Successfully cleaned up instance {instance_id}") + # 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 {instance_id}: {e}") + print(f"Error during cleanup of {process_id}: {e}") finally: # Always remove from tracking - del self.instances[instance_id] - print(f"Removed instance {instance_id}. Remaining instances: {len(self.instances)}") + del self.processes[process_id] + print( + f"Removed process {process_id}. Remaining processes: {len(self.processes)}" + ) else: - print(f"Instance {instance_id} not found during cleanup") + print(f"Process {process_id} not found during cleanup") def stop_all(self) -> None: - """Stop all playing sounds.""" + """Stop all playing sounds by killing VLC processes.""" with self.lock: - # Create a copy of the instances to avoid race conditions - instances_copy = dict(self.instances) - print(f"Stopping {len(instances_copy)} instances: {list(instances_copy.keys())}") - - for instance_id, instance_data in instances_copy.items(): + 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: - player = instance_data["player"] - instance = instance_data["instance"] - - print(f"Stopping instance {instance_id}") - - # Force stop the player regardless of state - player.stop() - - # Give VLC a moment to process the stop command - time.sleep(0.1) - - # Release the media player and instance - player.release() - instance.release() - - print(f"Successfully stopped instance {instance_id}") - + 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: - # Log the error but continue stopping other instances - print(f"Error stopping instance {instance_id}: {e}") - - # Clear all instances - self.instances.clear() - print(f"Cleared all instances. Remaining: {len(self.instances)}") + 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: - return len(self.instances) - + # 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 and clean up resources. Returns count of stopped instances.""" + """Force stop all sounds by killing VLC processes aggressively.""" with self.lock: - stopped_count = len(self.instances) - print(f"Force stopping {stopped_count} instances: {list(self.instances.keys())}") - - # More aggressive cleanup - for instance_id, instance_data in list(self.instances.items()): - try: - player = instance_data["player"] - instance = instance_data["instance"] - - print(f"Force stopping instance {instance_id}") - - # Multiple stop attempts - for attempt in range(3): - if hasattr(player, 'stop'): - player.stop() - print(f"Stop attempt {attempt + 1} for {instance_id}") - time.sleep(0.05) # Short delay between attempts - - # Force release - if hasattr(player, 'release'): - player.release() - print(f"Released player for {instance_id}") - if hasattr(instance, 'release'): - instance.release() - print(f"Released instance for {instance_id}") - - except Exception as e: - print(f"Error force-stopping instance {instance_id}: {e}") - finally: - # Always remove from tracking - if instance_id in self.instances: - del self.instances[instance_id] - print(f"Removed {instance_id} from tracking") - - print(f"Force stop completed. Instances remaining: {len(self.instances)}") + 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 diff --git a/pyproject.toml b/pyproject.toml index 769269f..1c41ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ dependencies = [ "flask-sqlalchemy==3.1.1", "pydub==0.25.1", "python-dotenv==1.1.1", - "python-vlc>=3.0.0", "requests==2.32.4", "werkzeug==3.1.3", ] diff --git a/uv.lock b/uv.lock index d74dd83..9a1860e 100644 --- a/uv.lock +++ b/uv.lock @@ -526,15 +526,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, ] -[[package]] -name = "python-vlc" -version = "3.0.21203" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/5b/f9ce6f0c9877b6fe5eafbade55e0dcb6b2b30f1c2c95837aef40e390d63b/python_vlc-3.0.21203.tar.gz", hash = "sha256:52d0544b276b11e58b6c0b748c3e0518f94f74b1b4cd328c83a59eacabead1ec", size = 162211 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/ee/7d76eb3b50ccb1397621f32ede0fb4d17aa55a9aa2251bc34e6b9929fdce/python_vlc-3.0.21203-py3-none-any.whl", hash = "sha256:1613451a31b692ec276296ceeae0c0ba82bfc2d094dabf9aceb70f58944a6320", size = 87651 }, -] - [[package]] name = "requests" version = "2.32.4" @@ -590,7 +581,6 @@ dependencies = [ { name = "flask-sqlalchemy" }, { name = "pydub" }, { name = "python-dotenv" }, - { name = "python-vlc" }, { name = "requests" }, { name = "werkzeug" }, ] @@ -614,7 +604,6 @@ requires-dist = [ { name = "flask-sqlalchemy", specifier = "==3.1.1" }, { name = "pydub", specifier = "==0.25.1" }, { name = "python-dotenv", specifier = "==1.1.1" }, - { name = "python-vlc", specifier = ">=3.0.0" }, { name = "requests", specifier = "==2.32.4" }, { name = "werkzeug", specifier = "==3.1.3" }, ]