Another attempt at memory management. It hasn't failed yet but then again I haven't pushed yet, so here's the ultimate test.

This commit is contained in:
Storm Dragon
2025-11-04 00:39:33 -05:00
parent 2ae85fe25c
commit 8bcde8bf37
2 changed files with 64 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ Supports real-time speed control without re-encoding.
import os
from pathlib import Path
import threading
from concurrent.futures import ThreadPoolExecutor
try:
import mpv
@@ -34,6 +35,8 @@ class MpvPlayer:
self.currentPlaylistIndex = 0 # Current file index in playlist
self.activeTempFiles = [] # Track temp files for cleanup
self.tempFileLock = threading.Lock() # Protect temp file list
# Thread pool for cleanup tasks (prevents daemon thread accumulation)
self.cleanupExecutor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="mpv-cleanup")
if not HAS_MPV:
print("Warning: python-mpv not installed. Audio playback will not work.")
@@ -49,6 +52,10 @@ class MpvPlayer:
video=False, # Audio only
ytdl=False, # Don't use youtube-dl
quiet=True, # Minimal console output
demuxer_max_bytes='20M', # Reduced from 50M - limit demuxer cache
demuxer_max_back_bytes='5M', # Reduced from 10M - limit backward cache
cache=False, # Disable additional caching for local files
audio_buffer=0.5, # Reduce audio buffer to 0.5 seconds (default is 2s)
)
# Register event handler for cleanup
@@ -102,11 +109,11 @@ class MpvPlayer:
success = self.play_audio_file()
# Schedule cleanup after mpv loads the file
# Use shorter delay and clean up old files too
# Use ThreadPoolExecutor instead of daemon threads to prevent accumulation
if tempFile:
import time
def cleanup_temp_file(filepath):
time.sleep(2) # Reduced from 5s - mpv loads files quickly
time.sleep(2) # mpv loads files quickly
try:
os.unlink(filepath)
except:
@@ -117,7 +124,8 @@ class MpvPlayer:
self.activeTempFiles.remove(filepath)
except ValueError:
pass # Already removed
threading.Thread(target=cleanup_temp_file, args=(tempFile.name,), daemon=True).start()
# Use thread pool instead of creating new daemon threads
self.cleanupExecutor.submit(cleanup_temp_file, tempFile.name)
return success
@@ -263,6 +271,7 @@ class MpvPlayer:
def _cleanup_temp_files(self):
"""Immediately clean up all active temp files"""
import gc
with self.tempFileLock:
for tempPath in self.activeTempFiles:
try:
@@ -273,6 +282,41 @@ class MpvPlayer:
pass
# Clear the list
self.activeTempFiles = []
# CRITICAL: Force FULL garbage collection to free memory from deleted files
# After 30+ minutes of reading, temp file handles accumulate in older generations
gc.collect() # Full collection across all generations
def _purge_mpv_buffers(self):
"""
Aggressively purge mpv internal buffers and caches
Call this between paragraphs to prevent memory accumulation
"""
if not self.isInitialized or not self.player:
return
try:
# Stop any playback first to release audio buffers
self.player.stop()
# Wait a moment for mpv to fully stop and release buffers
import time
time.sleep(0.05)
# Force mpv to clear its internal demuxer cache
# This releases memory held by the demuxer
try:
# Setting these properties forces mpv to flush caches
# pylint: disable=no-member
self.player.command('seek', 0, 'absolute') # Dummy seek to flush
except:
pass # Might fail if nothing is loaded, that's ok
# Mark as unloaded so next load is clean
self.audioFileLoaded = False
except Exception as e:
# Don't fail if buffer purge fails
pass
def cleanup(self):
"""Cleanup resources"""
@@ -288,6 +332,11 @@ class MpvPlayer:
self.isInitialized = False
# Clean up any remaining temp files
self._cleanup_temp_files()
# Shutdown cleanup thread pool (wait for pending tasks to finish)
try:
self.cleanupExecutor.shutdown(wait=True, cancel_futures=False)
except:
pass
def is_available(self):
"""Check if mpv is available"""
@@ -314,6 +363,11 @@ class MpvPlayer:
audioPath = str(audioPath)
self.playbackSpeed = max(0.5, min(2.0, float(playbackSpeed)))
# Aggressively purge mpv buffers before loading new file
# This prevents memory accumulation over long reading sessions
if self.audioFileLoaded:
self._purge_mpv_buffers()
# Check if this is a URL (for streaming from Audiobookshelf)
isUrl = audioPath.startswith('http://') or audioPath.startswith('https://')
@@ -365,6 +419,10 @@ class MpvPlayer:
self.currentPlaylistIndex = 0
self.playbackSpeed = max(0.5, min(2.0, float(playbackSpeed)))
# Aggressively purge mpv buffers before loading new playlist
if self.audioFileLoaded:
self._purge_mpv_buffers()
# Load first file in playlist
firstFile = self.playlist[0]