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.
This commit is contained in:
194
app/services/vlc_service.py
Normal file
194
app/services/vlc_service.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user