Multiple MediaCtrl installations cause hangs

Hi friends,

I am making a video viewer/annotator app. This app accepts dragging and dropping, tiles videos of a few seconds, and allows users to click them. The problem is that when I increase the tiling from 12 (4*3) to 15 (3x5) or 16 (4x4), the app hangs up. Until 12, the app works. According to my dirty print debugging, videos are successfully loaded using MediaCtrl.Load() . What is the cause of this problem, and what is the fix? Thanks!

Mac OS Ventura
Python 3.9.16 + miniconda
invoke the app using pythonw

App code here

import os
import wx
import wx.media
import csv

class ImagePanel(wx.Panel):
    def __init__(self, parent, imagePath, app_instance):
        super().__init__(parent, size=(150, 150))
        self.SetBackgroundColour('white')
        
        self.app = app_instance
        self.imagePath = imagePath
        self.mc = wx.media.MediaCtrl(self, style=wx.SIMPLE_BORDER, size=(140,100))
        self.quote = wx.StaticText(self, label=os.path.basename(self.imagePath), pos=(0, 120))
        self.Bind(wx.media.EVT_MEDIA_LOADED, self.play)
        self.Bind(wx.media.EVT_MEDIA_FINISHED, self.replay)
        
        self.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
        self.parent = parent
        self.isSelected = False

        if imagePath in self.app.selected_images:
            self.SetBackgroundColour('red')
            self.isSelected = True
        wx.CallAfter(self.load, self.imagePath)

    def load(self, path):
        if self.mc.Load(path):
            print('load ok')
            pass
        else:
            print("Media not found")
            self.quit(None)

    def play(self, evt):
        if self.mc.Play():
            print('play ok')
            pass
        else:
            print("bad playing")

    def replay(self, event):
        self.mc.Stop()
        print(self.mc.Play())

    def OnClick(self, event):
        self.isSelected = not self.isSelected
        if self.isSelected:
            self.SetBackgroundColour('red')
            self.app.selected_images.add(self.imagePath)
        else:
            self.SetBackgroundColour('white')
            self.app.selected_images.discard(self.imagePath)
        self.Refresh()

class App(wx.Frame):
    def __init__(self, parent, id, title):
        super().__init__(parent, id, title, size=(800, 800), style=wx.DEFAULT_FRAME_STYLE)
        self.selected_images = set()
        self.cols = 4
        self.rows = 3 # changing from 3 to 4 causes crashes
        self.InitUI()
        self.current_page = 0
        self.images_per_page = self.cols * self.rows
        self.total_images = []
        
        self.SetDropTarget(FileDropTarget(self))
        self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPress)
        
        self.Show()

    def InitUI(self):
        self.control_panel = wx.Panel(self)
        self.scroll_panel = wx.ScrolledWindow(self)
        self.scroll_panel.SetScrollbars(1, 1, 1000, 1000)
        
        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.images_sizer = wx.GridSizer(cols=self.cols, rows= self.rows, hgap=5, vgap=5)
        
        self.prev_button = wx.Button(self.control_panel, label='Prev')
        self.next_button = wx.Button(self.control_panel, label='Next')
        self.page_text = wx.StaticText(self.control_panel, label="Page 0/0")
        
        self.button_sizer.Add(self.prev_button, 0, wx.ALL, 5)
        self.button_sizer.Add(self.next_button, 0, wx.ALL, 5)
        self.button_sizer.Add(self.page_text, 0, wx.ALL, 5)
        
        self.control_panel.SetSizer(self.button_sizer)
        
        self.scroll_panel.SetSizer(self.images_sizer)
        
        self.main_sizer.Add(self.control_panel, 0, wx.EXPAND | wx.ALL, 5)
        self.main_sizer.Add(self.scroll_panel, 1, wx.EXPAND | wx.ALL, 5)
        
        self.SetSizer(self.main_sizer)
        
        self.prev_button.Bind(wx.EVT_BUTTON, self.OnPrev)
        self.next_button.Bind(wx.EVT_BUTTON, self.OnNext)
        
        self.Layout()

    def UpdatePageText(self):
        total_pages = (len(self.total_images) - 1) // self.images_per_page + 1
        self.page_text.SetLabel(f"Page {self.current_page + 1}/{total_pages}")

    def ShowImages(self, images):
        self.total_images = images
        self.current_page = 0
        self.UpdatePageText()
        self.ShowImagesPage(self.current_page)

    def ShowImagesPage(self, page):
        start_index = page * self.images_per_page
        end_index = start_index + self.images_per_page
        page_images = self.total_images[start_index:end_index]
        
        for child in self.scroll_panel.GetChildren():
            child.Destroy()
        self.images_sizer.Clear(True)

        for img_path in page_images:
            panel = ImagePanel(self.scroll_panel, img_path, self)
            self.images_sizer.Add(panel, 0, wx.ALL, 5)
        
        self.scroll_panel.Layout()
        self.images_sizer.Layout()
        self.UpdatePageText()

    def OnPrev(self, event):
        if self.current_page > 0:
            self.current_page -= 1
            self.ShowImagesPage(self.current_page)

    def OnNext(self, event):
        if (self.current_page + 1) * self.images_per_page < len(self.total_images):
            self.current_page += 1
            self.ShowImagesPage(self.current_page)

    def OnKeyPress(self, event):
        if event.GetKeyCode() == ord('E'):
            if self.total_images:
                first_folder = os.path.dirname(self.total_images[0])
                csv_path = os.path.join(first_folder, 'image_selection.csv')
                
                with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
                    fieldnames = ['filename', 'value']
                    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
                    
                    writer.writeheader()
                    for image_path in self.total_images:
                        filename = os.path.basename(image_path)
                        value = 1 if image_path in self.selected_images else 0
                        writer.writerow({'filename': filename, 'value': value})
                
                print(f"CSV file has been saved to: {csv_path}")
            else:
                print("No images were loaded.")
            
            self.Close(True)
        event.Skip()

class FileDropTarget(wx.FileDropTarget):
    def __init__(self, window):
        super().__init__()
        self.window = window

    def OnDropFiles(self, x, y, folders):
        if len(folders) != 1 or not os.path.isdir(folders[0]):
            print("Please drop a single folder.")
            return False
        
        data_path = os.path.join(folders[0], 'data')
        if not os.path.exists(data_path):
            self.window.Destroy()

        images = []

        for file in os.listdir(os.path.join(folders[0], "data")):
            if file.lower().endswith(('.mp4')):
                images.append(os.path.join(folders[0], "data",file))
        images.sort()
        self.window.ShowImages(images)
        return True

app = wx.App(False)
App(None, -1, 'Viewer')
app.MainLoop()

For creating test data, I used the following code.

import os
from PIL import Image, ImageDraw, ImageFont
import av

width, height = 384, 192
duration = 4
fps = 120
total_frames = duration * fps


total_videos = 16
for i in range(total_videos):
    output_filename = f'output_video_{i}.mp4'
    container = av.open(output_filename, mode='w')

    stream = container.add_stream('libx264', rate=fps)
    stream.width = width
    stream.height = height
    stream.pix_fmt = 'yuv420p'

    for frame_number in range(total_frames):
        brightness = int(255 * (20 + (60 * frame_number / total_frames)) / 100)
        image = Image.new('RGB', (width, height), (brightness, brightness, brightness))
        draw = ImageDraw.Draw(image)

        font = ImageFont.load_default()
        text = str(frame_number)
        draw.text((100,100), text, fill=(255, 255, 255), font=font)
        draw.text((200,100), 'video number' + str(i), fill=(255, 255, 255), font=font)

        frame = av.VideoFrame.from_image(image)

        for packet in stream.encode(frame):
            container.mux(packet)

    for packet in stream.encode():
        container.mux(packet)

    container.close()

print('Finish creating videos')