Zach Burke
Zach Burke

Reputation: 1

Why is my VLC Media Player application leaking GPU memory on Raspberry Pi 4B+ and 5?

We have a Python-based media player application that loops through media files and displays them on a screen.

After running anywhere from ~3-24 hours, the display will sometimes freeze while playing a video. When this happens, dmesg shows the following messages (these particular messages are from when we were still using drm for the video output. We now use xcb_xv , but we have the same problem):

...
...
[    3.028998] vc4-drm gpu: [drm] fb0: vc4drmfb frame buffer device
[ 9139.538040] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9139.545459] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9139.552522] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9139.583064] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9151.090537] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9151.096344] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9191.649563] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
[ 9191.675813] vc4-drm gpu: [drm] *ERROR* Failed to allocate DLIST entry. Requested size=9. ret=-28
...
...

This indicates that GPU memory has been leaked and there is no longer enough available to continue playing the videos. The device (Raspberry Pi) continues to function otherwise.

For video files, VLC is used for playback. At the end of each video, we attempt to clean up all resources VLC used to display the video. We have also tried keeping a video play counter, which we used to completely tear down and rebuild the VLC player instance after every 50 video plays. This did not resolve the issue.

Here is the code:

import os
import time
import json
import vlc
import subprocess
from pathlib import Path
import pygame
from pygame import display, image, time as pygame_time
from jplayer.utils import scene_update_flag_utils as sufu
from jplayer.utils import logging_utils as lu
from jplayer.utils import system_utils as su
from jplayer.clients.japi_client import JApiClient

logger = lu.get_logger("jplayer_app")

VIDEO_REINIT_THRESHOLD = 50


class MediaPlayer:
    def __init__(self, media_directory, scenes_directory):
        logger.info("Initializing MediaPlayer ...")
        self.media_directory = Path(media_directory)
        self.scenes_directory = Path(scenes_directory)
        self.default_image_path = Path('/home/static_images/ready_for_content.jpeg')
        self.supported_images = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.heic'}
        self.supported_videos = {'.mp4', '.avi', '.mov'}
        self.current_scenes = []
        self.video_count = 0

        # Get orientation
        self.jclient = JApiClient(logger)
        orientation = self.jclient.jplayer_info.get("orientation", "LANDSCAPE")
        logger.info(f"    Detected Orientation: {orientation}")
        self.screen_config = su.get_screen_config(orientation)

        # Initialize Pygame for image display
        pygame.init()
        display.init()

        # Set up fullscreen display with proper orientation
        display_dims = self.screen_config.get_display_dimensions()
        self.screen = display.set_mode(display_dims, pygame.FULLSCREEN)
        self.screen_width, self.screen_height = display_dims

        # Initialize VLC
        self.initialize_vlc()

        logger.info("Done initializing MediaPlayer.")

    def initialize_vlc(self):
        """Initialize the VLC instance and player."""
        vlc_args = [
            '--vout=xcb_xv',
            '--no-video-title-show',
            '--file-caching=2000'
        ]
        self.vlc_instance = vlc.Instance(vlc_args)
        self.vlc_player = self.vlc_instance.media_player_new()

        # Tell VLC to render to the pygame window
        if os.name == 'nt':  # Windows
            self.vlc_player.set_hwnd(pygame.display.get_wm_info()['window'])
        else:  # Linux/Raspberry Pi
            self.vlc_player.set_xwindow(pygame.display.get_wm_info()['window'])

    def reinitialize_vlc(self):
        """Reinitialize VLC after releasing resources to free up GPU memory."""
        logger.info("Reinitializing VLC to reclaim GPU resources...")
        self.vlc_player.release()
        self.vlc_instance.release()
        self.initialize_vlc()
        self.video_count = 0  # Reset the counter after reinitialization
        logger.info("VLC reinitialized successfully.")

    def load_scenes(self):
        """Load and parse scene configuration files."""
        scenes = []
        for file in self.scenes_directory.glob('*.json'):
            try:
                with open(file, 'r') as f:
                    scene = json.load(f)
                    required_fields = {'id', 'media_file', 'order'}
                    if not all(field in scene for field in required_fields):
                        logger.info(f"Scene file {file} missing required fields")
                        continue

                    media_path = self.media_directory / scene['media_file']
                    if not media_path.exists():
                        logger.info(f"Media file not found: {media_path}")
                        continue

                    scene['media_path'] = media_path
                    scenes.append(scene)
            except Exception as e:
                logger.error(f"Error loading scene file {file}: {e}")
                continue

        return sorted(scenes, key=lambda x: x['order'])

    def display_image(self, scene):
        """Display an image for the specified duration."""
        try:
            img = image.load(str(scene['media_path']))
            img = pygame.transform.scale(img, (self.screen_width, self.screen_height))

            if self.screen_config.get_pygame_rotation() != 0:
                img = pygame.transform.rotate(img, self.screen_config.get_pygame_rotation())

            self.screen.blit(img, (0, 0))
            display.flip()

            duration = int(scene.get('time_to_display', 7))
            pygame_time.wait(duration * 1000)
            return True
        except Exception as e:
            logger.info(f"Error displaying image {scene['media_path']}: {e}")
            return False

    def display_video(self, scene):
        """Play a video file using VLC."""
        media = None
        try:
            # Stop any existing playback and force cleanup
            if self.vlc_player.is_playing():
                self.vlc_player.stop()

            # Create new media instance
            media = self.vlc_instance.media_new(str(scene['media_path']))
            media.add_option(":no-audio")  # Disable audio processing if not needed
            media.add_option(":no-metadata-network-access")  # Prevent network access
            media.add_option(":no-video-title-show")  # Disable title display

            # Set media and immediately check if it was successful
            self.vlc_player.set_media(media)
            if not self.vlc_player.get_media():
                logger.info("    Failed to set media")
                return False

            # Attempt playback
            play_result = self.vlc_player.play()
            if play_result == -1:
                logger.info(f"    Failed to start playback for scene {scene.get('id')}")
                return False

            time.sleep(1)

            while self.vlc_player.is_playing():
                logger.info("    video is playing")
                pygame.event.pump()
                time.sleep(0.25)

                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        self.vlc_player.stop()
                        if media:
                            media.release()
                        return False

            # Explicitly stop playback
            self.vlc_player.stop()

            # Small delay to ensure cleanup
            time.sleep(0.1)

            # If we got here, the video played successfully
            self.video_count += 1
            if self.video_count >= VIDEO_REINIT_THRESHOLD:
                self.reinitialize_vlc()

            return True

        except Exception as e:
            logger.error(f"Error playing video {scene.get('id')}: {e}")
            return False

        finally:
            # Aggressive cleanup
            if self.vlc_player.is_playing():
                self.vlc_player.stop()
            # Force VLC to release GPU resources
            self.vlc_player.set_media(None)
            if media:
                media.release()
                media = None

    def display_default_scene(self):
        """Display the default 'ready for content' scene."""
        try:
            if not self.default_image_path.exists():
                logger.error(f"Default image not found at {self.default_image_path}")
                return False

            img = image.load(str(self.default_image_path))
            img = pygame.transform.scale(img, (self.screen_width, self.screen_height))

            if self.screen_config.get_pygame_rotation() != 0:
                img = pygame.transform.rotate(img, self.screen_config.get_pygame_rotation())

            self.screen.blit(img, (0, 0))
            display.flip()
            pygame_time.wait(7000)
            return True
        except Exception as e:
            logger.error(f"Error displaying default image: {e}")
            return False

    def run(self):
        """Main loop to display media files."""
        try:
            while True:
                if sufu.should_reload_scenes():
                    self.current_scenes = self.load_scenes()
                    logger.info("Reloaded scenes due to update flag")
                    sufu.reset_update_flag_to_zero()
                elif not self.current_scenes:
                    self.current_scenes = self.load_scenes()
                    logger.info("Initial scene load")

                if not self.current_scenes:
                    logger.info("No valid scenes found, displaying default scene")
                    self.display_default_scene()
                    time.sleep(15)
                    continue

                logger.info("Iterating through scenes ...")
                for scene in self.current_scenes:
                    logger.info(f"    Displaying scene {scene.get('id')} at media path {scene.get('media_path')}")
                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:
                            return

                    suffix = scene['media_path'].suffix.lower()
                    if suffix in self.supported_images:
                        if not self.display_image(scene):
                            continue
                    elif suffix in self.supported_videos:
                        if not self.display_video(scene):
                            continue

        finally:
            self.vlc_player.release()
            pygame.quit()


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description='Display images and videos in a loop based on scene configurations')
    parser.add_argument('media_directory', help='Directory containing media files (images and videos)')
    parser.add_argument('scenes_directory', help='Directory containing scene configuration JSON files')

    args = parser.parse_args()

    player = MediaPlayer(args.media_directory, args.scenes_directory)
    player.run()

We've tried to clean up resources as aggressively as we know how/are able to. Unfortunately, we're still experiencing the leak. We've also tried increasing GPU memory which did not resolve the issue.

Upvotes: 0

Views: 62

Answers (0)

Related Questions