Pyside 6.5.1.1 - QmediaPlayer setPosition locks theapp
-
This post is deleted!
-
The original problem was QmediaPlayer.setPosition led to the app locking up eventually.
The code below has passed testing which the original code would not. Does not necessarily pass the "fixed yeah" stage as I am suspicious still.
So what was done:?
All media_player methods were bundled into one class
_Video_PlayerClass Video_Handler was adjusted accordingly
The key part is in the Video_Handler _setup_media_player method. In the snippet below you will note the two sleep statements , one after moving to thread and one after thread.start. With these no app lockups have yet to be observed.
Hope this helps some one!
self._media_player.moveToThread(self._thread) sleep( 0.1 ) # Note: These are important, without them setPosition in the media player sometimes locks the app media_player_thread = self._media_player.thread() is_in_main_thread = media_player_thread == qtC.QThread.currentThread() print(f"Is in main thread: {is_in_main_thread}") self._thread.start() sleep( 0.5 ) # Note: These are important, without them setPosition in the media player sometimes locks the app
class _Video_Player(qtC.QObject): """ Implements a customer video player object """ current_frame_handler = qtC.Signal(int) duration_changed_handler = qtC.Signal(int) frame_changed_handler = qtC.Signal(qtM.QVideoFrame) is_available_handler = qtC.Signal(bool) media_status_changed_handler = qtC.Signal(qtM.QMediaPlayer.MediaStatus) pause_handler = qtC.Signal() play_handler = qtC.Signal() position_changed_handler = qtC.Signal(int) seekable_changed_handler = qtC.Signal(bool) set_position_handler = qtC.Signal(int) stop_handler = qtC.Signal() def __init__(self, parent: qtC.QObject | None, input_file: str) -> None: """ Sets up the video_player object for use Args: parent (qtC.QObject | None): Set the parent of the object input_file (str): Set the source file of the media player """ assert parent is None or isinstance( parent, qtC.QObject ), f"{parent =} must be None or a qtC.QObject" assert ( isinstance(input_file, str) and input_file.strip() != "" ), f"{input_file =} must be a non-empty str" super().__init__(parent) self._current_position = -1 self._video_sink = qtM.QVideoSink() self._audio_output = qtM.QAudioOutput() self._media_player = qtM.QMediaPlayer() self._media_player.setVideoSink(self._video_sink) self._media_player.setAudioOutput(self._audio_output) self._audio_output.setVolume(1) # Set input source video file self._media_player.setSource(qtC.QUrl.fromLocalFile(input_file)) # Hook up signals self._video_sink.videoFrameChanged.connect(self._frame_handler) self._media_player.durationChanged.connect(self._duration_changed) self._media_player.positionChanged.connect(self._position_changed) self._media_player.errorOccurred.connect(self._player_error) self._media_player.mediaStatusChanged.connect(self._media_status_change) self._media_player.seekableChanged.connect(self._seekable_changed) self.frame_changed_handler.connect(self._video_sink.setVideoFrame) self.is_available_handler.connect(self._media_player.isAvailable) self.play_handler.connect(self._media_player.play) self.set_position_handler.connect(self.seek) self.pause_handler.connect(self._media_player.pause) self.stop_handler.connect(self.stop) @qtC.Slot() def _duration_changed(self, duration: int) -> None: """Handles a video duration change Args: duration (int): The length of the video """ self.duration_changed_handler.emit(duration) @qtC.Slot() def _frame_handler(self, frame: qtM.QVideoFrame) -> None: """Handles the video frame changing signal Args: frame (qtM.QVideoFrame): THe video frame to be displayed """ self.frame_changed_handler.emit(frame) @qtC.Slot() def _position_changed(self, position_milliseconds: int) -> None: """ Handles the position changing signal Args: position_milliseconds (int): The current position of the media player in milliseconds. """ self.position_changed_handler.emit(position_milliseconds) @qtC.Slot(qtM.QMediaPlayer.Error, str) def _player_error(self, error, error_string): """Called when the media player encounters an error.""" print(f"Error: {error} - {error_string}") def available(self) -> bool: """ Returns whether the media player is available Returns: bool: True if Available, False if noe """ return self._media_player.isAvailable() def current_frame(self) -> int: return self._media_player.position() @qtC.Slot() def _media_status_change(self, media_status: qtM.QMediaPlayer.mediaStatus) -> None: """Signals the state of the media has changed Args: media_status (qtM.QMediaPlayer.mediaStatus): The status of the media player """ self.media_status_changed_handler.emit(media_status) @qtC.Slot() def _seekable_changed(self, seekable: bool) -> None: """ Signals the seekable status has changed Args: seekable (bool): True if the media player is seekable, False otherwise. """ self.seekable_changed_handler.emot(seekable) @qtC.Slot() def seek(self, position: int) -> None: """ Seeks to a position Args: position (int): THe position in milliseconds to move to """ if self._current_position != position: if ( self._media_player.isSeekable() and self._media_player.mediaStatus() == qtM.QMediaPlayer.MediaStatus.BufferedMedia ): self._current_position = position self._media_player.setPosition(position) def state(self) -> str: playback_state = self._media_player.playbackState() if playback_state == qtM.QMediaPlayer.PlaybackState.PlayingState: return "playing" elif playback_state == qtM.QMediaPlayer.PlaybackState.PausedState: return "paused" elif playback_state == qtM.QMediaPlayer.PlaybackState.StoppedState: return "stop" def stop(self): self._media_player.stop() self._media_player.setVideoSink(None) self._media_player.setAudioOutput(None) @dataclasses.dataclass class Video_Handler: aspect_ratio: str input_file: str output_edit_folder: str encoding_info: Encoding_Details video_display: qtg.Label video_slider: qtg.Slider frame_display: qtg.LCD display_width: int display_height: int update_slider: bool = True source_state: Literal[ "NoMedia", "Loading", "Loaded", "Stalled", "Buffering", "Buffered", "EndOfMedia", "InvalidMedia", ] = "NoMedia" state_handler: Callable = None # Private instance variables _frame_count: int = 0 _frame_rate: float = 25 # Default to 25 frames per second _frame_width: int = 720 _frame_height: int = 576 _current_frame: int = -1 def __post_init__(self) -> None: """Sets-up the instance""" assert isinstance(self.aspect_ratio, str) and self.aspect_ratio in ( sys_consts.AR169, sys_consts.AR43, ), f"{self.aspect_ratio=}. Must be a AR169 | AR43" assert ( isinstance(self.input_file, str) and self.input_file.strip() != "" ), f"{self.input_file=}. Must be a non-empty str" assert ( isinstance(self.output_edit_folder, str) and self.output_edit_folder.strip() != "" ), f"{self.output_edit_folder=}. Must be a non-empty str" assert isinstance( self.encoding_info, Encoding_Details ), f"{self.encoding_info=}. Must be an instance of Encoding_Details" assert isinstance( self.video_display, qtg.Label ), f"{self.video_display=}. Must be a qtg.Label" assert isinstance( self.video_slider, qtg.Slider ), f"{self.video_slider=}. Must be a qtg.Slider" assert isinstance( self.frame_display, qtg.LCD ), f"{self.frame_display=}. Must be a qtg.Slider" assert isinstance( self.display_width, int ), f"{self.display_width=}. Must be an int" assert isinstance( self.display_height, int ), f"{self.display_height=}. Must be an int" assert isinstance( self.update_slider, bool ), f"{self.update_slider=}. Must be a bool" self._frame_width = self.encoding_info.video_width self._frame_height = self.encoding_info.video_height self._frame_rate = self.encoding_info.video_frame_rate self._frame_count = self.encoding_info.video_frame_count self._setup_media_player() def _setup_media_player(self): """Sets up the media_player instance""" self._media_player = _Video_Player(parent=None, input_file=self.input_file) self._media_player.frame_changed_handler.connect(self._frame_handler) self._media_player.is_available_handler.connect(self.available) self._media_player.media_status_changed_handler.connect( self._media_status_change ) self._media_player.position_changed_handler.connect(self._position_changed) self._thread = qtC.QThread() self._media_player.moveToThread(self._thread) sleep( 0.1 ) # Note: These are important, without them setPosition in the media player sometimes locks the app media_player_thread = self._media_player.thread() is_in_main_thread = media_player_thread == qtC.QThread.currentThread() print(f"Is in main thread: {is_in_main_thread}") self._thread.start() sleep( 0.5 ) # Note: These are important, without them setPosition in the media player sometimes locks the app @qtC.Slot() def _frame_handler(self, frame: qtM.QVideoFrame) -> None: """Handles displaying the video frame Args: frame (qtM.QVideoFrame): THe video frame to be displayed """ if frame.isValid(): image = frame.toImage().scaled(self.display_width, self.display_height) pixmap = qtG.QPixmap.fromImage(image) if shiboken6.isValid( self.video_display.guiwidget_get ): # Should not need this check but on shutdown I sometimes got the dreaded C++ object deleted error self.video_display.guiwidget_get.setPixmap(pixmap) def _media_status_change(self, media_status: qtM.QMediaPlayer.mediaStatus) -> None: """When the status of the media player changes this method sets the source_state var and calls the state_handler if provided. Args: media_status (qtM.QMediaPlayer.mediaStatus): The status of the media player """ match media_status: case qtM.QMediaPlayer.MediaStatus.NoMedia: self.source_state = "NoMedia" case qtM.QMediaPlayer.MediaStatus.LoadingMedia: self.source_state = "Loading" case qtM.QMediaPlayer.MediaStatus.LoadedMedia: self.source_state = "Loaded" case qtM.QMediaPlayer.MediaStatus.StalledMedia: self.source_state = "Stalled" case qtM.QMediaPlayer.MediaStatus.BufferingMedia: self.source_state = "Buffering" case qtM.QMediaPlayer.MediaStatus.BufferedMedia: self.source_state = "Buffered" case qtM.QMediaPlayer.MediaStatus.EndOfMedia: self.source_state = "EndOfMedia" case qtM.QMediaPlayer.MediaStatus.InvalidMedia: self.source_state = "InvalidMedia" if self.state_handler and isinstance(self.state_handler, Callable): self.state_handler() @qtC.Slot() def _position_changed(self, position_milliseconds: int) -> None: """ A method that is called when the position of the media player changes. Converts the current position in milliseconds to the corresponding frame number, updates the video slider if necessary, and emits a signal indicating that the position has changed. Args: position_milliseconds (int): The current position of the media player in milliseconds. """ frame_number = int(position_milliseconds * self._frame_rate // 1000) if self.update_slider and self.video_slider is not None: self.video_slider.value_set(frame_number) self.frame_display.value_set(frame_number) def get_current_frame(self) -> int: """ Returns the current frame number based on the current position of the media player and the frame rate of the video. Returns: int: The current frame number. """ return int(self._media_player.current_frame() * self._frame_rate // 1000) def available(self) -> bool: """Checks if the media player is supported on the platform Returns: bool: True if the media player is supported, False otherwise. """ return self._media_player.available() def play(self) -> None: """ Starts playing the media. """ self._media_player.play_handler.emit() return None def pause(self) -> None: """ Pauses the media. """ self._media_player.pause_handler.emit() def seek(self, frame: int) -> None: """ Seeks to the specified frame number. Args: frame (int): The frame number to seek to. """ if self._current_frame != frame: state = self._media_player.state() if state == "playing": self._media_player.pause_handler.emit() sleep(0.2) self._current_frame = frame time_offset = int((1000 / self._frame_rate) * frame) self._media_player.set_position_handler.emit(time_offset) if state == "playing": pass # self._media_player.play_handler.emit() # Leads to stuttering video sometimes def shutdown(self) -> None: """ Stops playing the media and releases the player's resources. """ if self._media_player is not None: self._media_player.stop_handler.emit() if self._thread and self._thread.isRunning(): self._thread.quit() self._thread.wait() self._thread.deleteLater() self._thread = None return None def state(self) -> str: """ Returns the current playback state of the media player. Returns: str: The current playback state - "playing": The media player is currently playing. - "paused": The media player is currently paused. - "stop": The media player is currently stopped. """ return self._media_player.state()
-
This post is deleted!
This post is deleted! -
This post is deleted!
-
The original problem was QmediaPlayer.setPosition led to the app locking up eventually.
The code below has passed testing which the original code would not. Does not necessarily pass the "fixed yeah" stage as I am suspicious still.
So what was done:?
All media_player methods were bundled into one class
_Video_PlayerClass Video_Handler was adjusted accordingly
The key part is in the Video_Handler _setup_media_player method. In the snippet below you will note the two sleep statements , one after moving to thread and one after thread.start. With these no app lockups have yet to be observed.
Hope this helps some one!
self._media_player.moveToThread(self._thread) sleep( 0.1 ) # Note: These are important, without them setPosition in the media player sometimes locks the app media_player_thread = self._media_player.thread() is_in_main_thread = media_player_thread == qtC.QThread.currentThread() print(f"Is in main thread: {is_in_main_thread}") self._thread.start() sleep( 0.5 ) # Note: These are important, without them setPosition in the media player sometimes locks the app
class _Video_Player(qtC.QObject): """ Implements a customer video player object """ current_frame_handler = qtC.Signal(int) duration_changed_handler = qtC.Signal(int) frame_changed_handler = qtC.Signal(qtM.QVideoFrame) is_available_handler = qtC.Signal(bool) media_status_changed_handler = qtC.Signal(qtM.QMediaPlayer.MediaStatus) pause_handler = qtC.Signal() play_handler = qtC.Signal() position_changed_handler = qtC.Signal(int) seekable_changed_handler = qtC.Signal(bool) set_position_handler = qtC.Signal(int) stop_handler = qtC.Signal() def __init__(self, parent: qtC.QObject | None, input_file: str) -> None: """ Sets up the video_player object for use Args: parent (qtC.QObject | None): Set the parent of the object input_file (str): Set the source file of the media player """ assert parent is None or isinstance( parent, qtC.QObject ), f"{parent =} must be None or a qtC.QObject" assert ( isinstance(input_file, str) and input_file.strip() != "" ), f"{input_file =} must be a non-empty str" super().__init__(parent) self._current_position = -1 self._video_sink = qtM.QVideoSink() self._audio_output = qtM.QAudioOutput() self._media_player = qtM.QMediaPlayer() self._media_player.setVideoSink(self._video_sink) self._media_player.setAudioOutput(self._audio_output) self._audio_output.setVolume(1) # Set input source video file self._media_player.setSource(qtC.QUrl.fromLocalFile(input_file)) # Hook up signals self._video_sink.videoFrameChanged.connect(self._frame_handler) self._media_player.durationChanged.connect(self._duration_changed) self._media_player.positionChanged.connect(self._position_changed) self._media_player.errorOccurred.connect(self._player_error) self._media_player.mediaStatusChanged.connect(self._media_status_change) self._media_player.seekableChanged.connect(self._seekable_changed) self.frame_changed_handler.connect(self._video_sink.setVideoFrame) self.is_available_handler.connect(self._media_player.isAvailable) self.play_handler.connect(self._media_player.play) self.set_position_handler.connect(self.seek) self.pause_handler.connect(self._media_player.pause) self.stop_handler.connect(self.stop) @qtC.Slot() def _duration_changed(self, duration: int) -> None: """Handles a video duration change Args: duration (int): The length of the video """ self.duration_changed_handler.emit(duration) @qtC.Slot() def _frame_handler(self, frame: qtM.QVideoFrame) -> None: """Handles the video frame changing signal Args: frame (qtM.QVideoFrame): THe video frame to be displayed """ self.frame_changed_handler.emit(frame) @qtC.Slot() def _position_changed(self, position_milliseconds: int) -> None: """ Handles the position changing signal Args: position_milliseconds (int): The current position of the media player in milliseconds. """ self.position_changed_handler.emit(position_milliseconds) @qtC.Slot(qtM.QMediaPlayer.Error, str) def _player_error(self, error, error_string): """Called when the media player encounters an error.""" print(f"Error: {error} - {error_string}") def available(self) -> bool: """ Returns whether the media player is available Returns: bool: True if Available, False if noe """ return self._media_player.isAvailable() def current_frame(self) -> int: return self._media_player.position() @qtC.Slot() def _media_status_change(self, media_status: qtM.QMediaPlayer.mediaStatus) -> None: """Signals the state of the media has changed Args: media_status (qtM.QMediaPlayer.mediaStatus): The status of the media player """ self.media_status_changed_handler.emit(media_status) @qtC.Slot() def _seekable_changed(self, seekable: bool) -> None: """ Signals the seekable status has changed Args: seekable (bool): True if the media player is seekable, False otherwise. """ self.seekable_changed_handler.emot(seekable) @qtC.Slot() def seek(self, position: int) -> None: """ Seeks to a position Args: position (int): THe position in milliseconds to move to """ if self._current_position != position: if ( self._media_player.isSeekable() and self._media_player.mediaStatus() == qtM.QMediaPlayer.MediaStatus.BufferedMedia ): self._current_position = position self._media_player.setPosition(position) def state(self) -> str: playback_state = self._media_player.playbackState() if playback_state == qtM.QMediaPlayer.PlaybackState.PlayingState: return "playing" elif playback_state == qtM.QMediaPlayer.PlaybackState.PausedState: return "paused" elif playback_state == qtM.QMediaPlayer.PlaybackState.StoppedState: return "stop" def stop(self): self._media_player.stop() self._media_player.setVideoSink(None) self._media_player.setAudioOutput(None) @dataclasses.dataclass class Video_Handler: aspect_ratio: str input_file: str output_edit_folder: str encoding_info: Encoding_Details video_display: qtg.Label video_slider: qtg.Slider frame_display: qtg.LCD display_width: int display_height: int update_slider: bool = True source_state: Literal[ "NoMedia", "Loading", "Loaded", "Stalled", "Buffering", "Buffered", "EndOfMedia", "InvalidMedia", ] = "NoMedia" state_handler: Callable = None # Private instance variables _frame_count: int = 0 _frame_rate: float = 25 # Default to 25 frames per second _frame_width: int = 720 _frame_height: int = 576 _current_frame: int = -1 def __post_init__(self) -> None: """Sets-up the instance""" assert isinstance(self.aspect_ratio, str) and self.aspect_ratio in ( sys_consts.AR169, sys_consts.AR43, ), f"{self.aspect_ratio=}. Must be a AR169 | AR43" assert ( isinstance(self.input_file, str) and self.input_file.strip() != "" ), f"{self.input_file=}. Must be a non-empty str" assert ( isinstance(self.output_edit_folder, str) and self.output_edit_folder.strip() != "" ), f"{self.output_edit_folder=}. Must be a non-empty str" assert isinstance( self.encoding_info, Encoding_Details ), f"{self.encoding_info=}. Must be an instance of Encoding_Details" assert isinstance( self.video_display, qtg.Label ), f"{self.video_display=}. Must be a qtg.Label" assert isinstance( self.video_slider, qtg.Slider ), f"{self.video_slider=}. Must be a qtg.Slider" assert isinstance( self.frame_display, qtg.LCD ), f"{self.frame_display=}. Must be a qtg.Slider" assert isinstance( self.display_width, int ), f"{self.display_width=}. Must be an int" assert isinstance( self.display_height, int ), f"{self.display_height=}. Must be an int" assert isinstance( self.update_slider, bool ), f"{self.update_slider=}. Must be a bool" self._frame_width = self.encoding_info.video_width self._frame_height = self.encoding_info.video_height self._frame_rate = self.encoding_info.video_frame_rate self._frame_count = self.encoding_info.video_frame_count self._setup_media_player() def _setup_media_player(self): """Sets up the media_player instance""" self._media_player = _Video_Player(parent=None, input_file=self.input_file) self._media_player.frame_changed_handler.connect(self._frame_handler) self._media_player.is_available_handler.connect(self.available) self._media_player.media_status_changed_handler.connect( self._media_status_change ) self._media_player.position_changed_handler.connect(self._position_changed) self._thread = qtC.QThread() self._media_player.moveToThread(self._thread) sleep( 0.1 ) # Note: These are important, without them setPosition in the media player sometimes locks the app media_player_thread = self._media_player.thread() is_in_main_thread = media_player_thread == qtC.QThread.currentThread() print(f"Is in main thread: {is_in_main_thread}") self._thread.start() sleep( 0.5 ) # Note: These are important, without them setPosition in the media player sometimes locks the app @qtC.Slot() def _frame_handler(self, frame: qtM.QVideoFrame) -> None: """Handles displaying the video frame Args: frame (qtM.QVideoFrame): THe video frame to be displayed """ if frame.isValid(): image = frame.toImage().scaled(self.display_width, self.display_height) pixmap = qtG.QPixmap.fromImage(image) if shiboken6.isValid( self.video_display.guiwidget_get ): # Should not need this check but on shutdown I sometimes got the dreaded C++ object deleted error self.video_display.guiwidget_get.setPixmap(pixmap) def _media_status_change(self, media_status: qtM.QMediaPlayer.mediaStatus) -> None: """When the status of the media player changes this method sets the source_state var and calls the state_handler if provided. Args: media_status (qtM.QMediaPlayer.mediaStatus): The status of the media player """ match media_status: case qtM.QMediaPlayer.MediaStatus.NoMedia: self.source_state = "NoMedia" case qtM.QMediaPlayer.MediaStatus.LoadingMedia: self.source_state = "Loading" case qtM.QMediaPlayer.MediaStatus.LoadedMedia: self.source_state = "Loaded" case qtM.QMediaPlayer.MediaStatus.StalledMedia: self.source_state = "Stalled" case qtM.QMediaPlayer.MediaStatus.BufferingMedia: self.source_state = "Buffering" case qtM.QMediaPlayer.MediaStatus.BufferedMedia: self.source_state = "Buffered" case qtM.QMediaPlayer.MediaStatus.EndOfMedia: self.source_state = "EndOfMedia" case qtM.QMediaPlayer.MediaStatus.InvalidMedia: self.source_state = "InvalidMedia" if self.state_handler and isinstance(self.state_handler, Callable): self.state_handler() @qtC.Slot() def _position_changed(self, position_milliseconds: int) -> None: """ A method that is called when the position of the media player changes. Converts the current position in milliseconds to the corresponding frame number, updates the video slider if necessary, and emits a signal indicating that the position has changed. Args: position_milliseconds (int): The current position of the media player in milliseconds. """ frame_number = int(position_milliseconds * self._frame_rate // 1000) if self.update_slider and self.video_slider is not None: self.video_slider.value_set(frame_number) self.frame_display.value_set(frame_number) def get_current_frame(self) -> int: """ Returns the current frame number based on the current position of the media player and the frame rate of the video. Returns: int: The current frame number. """ return int(self._media_player.current_frame() * self._frame_rate // 1000) def available(self) -> bool: """Checks if the media player is supported on the platform Returns: bool: True if the media player is supported, False otherwise. """ return self._media_player.available() def play(self) -> None: """ Starts playing the media. """ self._media_player.play_handler.emit() return None def pause(self) -> None: """ Pauses the media. """ self._media_player.pause_handler.emit() def seek(self, frame: int) -> None: """ Seeks to the specified frame number. Args: frame (int): The frame number to seek to. """ if self._current_frame != frame: state = self._media_player.state() if state == "playing": self._media_player.pause_handler.emit() sleep(0.2) self._current_frame = frame time_offset = int((1000 / self._frame_rate) * frame) self._media_player.set_position_handler.emit(time_offset) if state == "playing": pass # self._media_player.play_handler.emit() # Leads to stuttering video sometimes def shutdown(self) -> None: """ Stops playing the media and releases the player's resources. """ if self._media_player is not None: self._media_player.stop_handler.emit() if self._thread and self._thread.isRunning(): self._thread.quit() self._thread.wait() self._thread.deleteLater() self._thread = None return None def state(self) -> str: """ Returns the current playback state of the media player. Returns: str: The current playback state - "playing": The media player is currently playing. - "paused": The media player is currently paused. - "stop": The media player is currently stopped. """ return self._media_player.state()
-