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:
+3
-2
@@ -2289,8 +2289,9 @@ class BookReader:
|
|||||||
wavData = self.ttsEngine.text_to_wav_data(paragraph)
|
wavData = self.ttsEngine.text_to_wav_data(paragraph)
|
||||||
|
|
||||||
if wavData:
|
if wavData:
|
||||||
# Stop any existing audio playback
|
# Stop any existing audio file (playing, paused, or idle)
|
||||||
if self.audioPlayer.is_audio_file_playing():
|
# This ensures temp files are cleaned up immediately
|
||||||
|
if self.audioPlayer.is_audio_file_loaded():
|
||||||
self.audioPlayer.stop_audio_file()
|
self.audioPlayer.stop_audio_file()
|
||||||
|
|
||||||
# Get current playback speed from config
|
# Get current playback speed from config
|
||||||
|
|||||||
+61
-3
@@ -10,6 +10,7 @@ Supports real-time speed control without re-encoding.
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import threading
|
import threading
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import mpv
|
import mpv
|
||||||
@@ -34,6 +35,8 @@ class MpvPlayer:
|
|||||||
self.currentPlaylistIndex = 0 # Current file index in playlist
|
self.currentPlaylistIndex = 0 # Current file index in playlist
|
||||||
self.activeTempFiles = [] # Track temp files for cleanup
|
self.activeTempFiles = [] # Track temp files for cleanup
|
||||||
self.tempFileLock = threading.Lock() # Protect temp file list
|
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:
|
if not HAS_MPV:
|
||||||
print("Warning: python-mpv not installed. Audio playback will not work.")
|
print("Warning: python-mpv not installed. Audio playback will not work.")
|
||||||
@@ -49,6 +52,10 @@ class MpvPlayer:
|
|||||||
video=False, # Audio only
|
video=False, # Audio only
|
||||||
ytdl=False, # Don't use youtube-dl
|
ytdl=False, # Don't use youtube-dl
|
||||||
quiet=True, # Minimal console output
|
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
|
# Register event handler for cleanup
|
||||||
@@ -102,11 +109,11 @@ class MpvPlayer:
|
|||||||
success = self.play_audio_file()
|
success = self.play_audio_file()
|
||||||
|
|
||||||
# Schedule cleanup after mpv loads the 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:
|
if tempFile:
|
||||||
import time
|
import time
|
||||||
def cleanup_temp_file(filepath):
|
def cleanup_temp_file(filepath):
|
||||||
time.sleep(2) # Reduced from 5s - mpv loads files quickly
|
time.sleep(2) # mpv loads files quickly
|
||||||
try:
|
try:
|
||||||
os.unlink(filepath)
|
os.unlink(filepath)
|
||||||
except:
|
except:
|
||||||
@@ -117,7 +124,8 @@ class MpvPlayer:
|
|||||||
self.activeTempFiles.remove(filepath)
|
self.activeTempFiles.remove(filepath)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass # Already removed
|
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
|
return success
|
||||||
|
|
||||||
@@ -263,6 +271,7 @@ class MpvPlayer:
|
|||||||
|
|
||||||
def _cleanup_temp_files(self):
|
def _cleanup_temp_files(self):
|
||||||
"""Immediately clean up all active temp files"""
|
"""Immediately clean up all active temp files"""
|
||||||
|
import gc
|
||||||
with self.tempFileLock:
|
with self.tempFileLock:
|
||||||
for tempPath in self.activeTempFiles:
|
for tempPath in self.activeTempFiles:
|
||||||
try:
|
try:
|
||||||
@@ -273,6 +282,41 @@ class MpvPlayer:
|
|||||||
pass
|
pass
|
||||||
# Clear the list
|
# Clear the list
|
||||||
self.activeTempFiles = []
|
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):
|
def cleanup(self):
|
||||||
"""Cleanup resources"""
|
"""Cleanup resources"""
|
||||||
@@ -288,6 +332,11 @@ class MpvPlayer:
|
|||||||
self.isInitialized = False
|
self.isInitialized = False
|
||||||
# Clean up any remaining temp files
|
# Clean up any remaining temp files
|
||||||
self._cleanup_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):
|
def is_available(self):
|
||||||
"""Check if mpv is available"""
|
"""Check if mpv is available"""
|
||||||
@@ -314,6 +363,11 @@ class MpvPlayer:
|
|||||||
audioPath = str(audioPath)
|
audioPath = str(audioPath)
|
||||||
self.playbackSpeed = max(0.5, min(2.0, float(playbackSpeed)))
|
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)
|
# Check if this is a URL (for streaming from Audiobookshelf)
|
||||||
isUrl = audioPath.startswith('http://') or audioPath.startswith('https://')
|
isUrl = audioPath.startswith('http://') or audioPath.startswith('https://')
|
||||||
|
|
||||||
@@ -365,6 +419,10 @@ class MpvPlayer:
|
|||||||
self.currentPlaylistIndex = 0
|
self.currentPlaylistIndex = 0
|
||||||
self.playbackSpeed = max(0.5, min(2.0, float(playbackSpeed)))
|
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
|
# Load first file in playlist
|
||||||
firstFile = self.playlist[0]
|
firstFile = self.playlist[0]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user