feat(vlc_service): refactor VLC service to use subprocess for sound playback and management; update process tracking
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
# 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()
|
||||
|
||||
# Release resources
|
||||
player.release()
|
||||
instance.release()
|
||||
print(f"Successfully cleaned up instance {instance_id}")
|
||||
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())}")
|
||||
processes_copy = dict(self.processes)
|
||||
print(
|
||||
f"Stopping {len(processes_copy)} VLC processes: {list(processes_copy.keys())}"
|
||||
)
|
||||
|
||||
for instance_id, instance_data in instances_copy.items():
|
||||
for process_id, process in processes_copy.items():
|
||||
try:
|
||||
player = instance_data["player"]
|
||||
instance = instance_data["instance"]
|
||||
if process.poll() is None: # Process is still running
|
||||
print(
|
||||
f"Terminating process {process.pid} ({process_id})"
|
||||
)
|
||||
process.terminate()
|
||||
|
||||
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}")
|
||||
# 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}")
|
||||
print(f"Error stopping process {process_id}: {e}")
|
||||
|
||||
# Clear all instances
|
||||
self.instances.clear()
|
||||
print(f"Cleared all instances. Remaining: {len(self.instances)}")
|
||||
# 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())}")
|
||||
stopped_count = len(self.processes)
|
||||
print(f"Force stopping {stopped_count} VLC processes")
|
||||
|
||||
# More aggressive cleanup
|
||||
for instance_id, instance_data in list(self.instances.items()):
|
||||
# # 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:
|
||||
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}")
|
||||
|
||||
subprocess.run(["pkill", "-f", "vlc"], check=False)
|
||||
print("Killed any remaining VLC processes system-wide")
|
||||
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"Error killing system VLC processes: {e}")
|
||||
|
||||
print(f"Force stop completed. Instances remaining: {len(self.instances)}")
|
||||
# Clear all processes
|
||||
self.processes.clear()
|
||||
print(
|
||||
f"Force stop completed. Processes remaining: {len(self.processes)}"
|
||||
)
|
||||
return stopped_count
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
11
uv.lock
generated
11
uv.lock
generated
@@ -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" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user