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:
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user