Files
sdb-back/app/services/vlc_service.py
JSC 7455811860 feat: Add VLC service for sound playback and management
- Implemented VLCService to handle sound playback using VLC.
- Added routes for soundboard management including play, stop, and status.
- Introduced admin routes for sound normalization and scanning.
- Updated user model and services to accommodate new functionalities.
- Enhanced error handling and logging throughout the application.
- Updated dependencies to include python-vlc for sound playback capabilities.
2025-07-03 21:25:50 +02:00

195 lines
7.3 KiB
Python

"""VLC service for playing sounds."""
import os
import threading
import time
import uuid
from typing import Any, Dict, Optional
import vlc
from app.database import db
from app.models.sound import Sound
class VLCService:
"""Service for playing sounds using VLC."""
def __init__(self) -> None:
"""Initialize VLC service."""
self.instances: Dict[str, Dict[str, Any]] = {}
self.lock = threading.Lock()
def play_sound(self, sound_id: int) -> bool:
"""Play a sound by ID using VLC."""
with self.lock:
# 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.type.lower(),
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
# Create VLC instance
instance = vlc.Instance()
player = instance.media_player_new()
# Load and play media
media = instance.media_new(sound_path)
player.set_media(media)
# Start playback
player.play()
# 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(),
}
print(f"Created instance {instance_id} for sound {sound.name}. Total instances: {len(self.instances)}")
# Increment play count
sound.increment_play_count()
# Schedule cleanup
threading.Thread(
target=self._cleanup_after_playback,
args=(instance_id, sound.duration if sound.duration else 10),
daemon=True,
).start()
return True
def _cleanup_after_playback(self, instance_id: str, duration: int) -> None:
"""Clean up VLC instance 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"]
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}")
except Exception as e:
print(f"Error during cleanup of {instance_id}: {e}")
finally:
# Always remove from tracking
del self.instances[instance_id]
print(f"Removed instance {instance_id}. Remaining instances: {len(self.instances)}")
else:
print(f"Instance {instance_id} not found during cleanup")
def stop_all(self) -> None:
"""Stop all playing sounds."""
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():
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}")
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)}")
def get_playing_count(self) -> int:
"""Get number of currently playing sounds."""
with self.lock:
return len(self.instances)
def force_stop_all(self) -> int:
"""Force stop all sounds and clean up resources. Returns count of stopped instances."""
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)}")
return stopped_count
# Global VLC service instance
vlc_service = VLCService()