diff --git a/script.audiooffsetmanager/README.md b/script.audiooffsetmanager/README.md index 413a5104a5..730fb12aa4 100644 --- a/script.audiooffsetmanager/README.md +++ b/script.audiooffsetmanager/README.md @@ -25,18 +25,20 @@ This addon streamlines your viewing experience by automating the process of audi ## Supported Formats ### Audio Formats -- Dolby Atmos / TrueHD -- Dolby Digital Plus (E-AC-3) +- Dolby TrueHD* +- Dolby Digital Plus (E-AC-3)* - Dolby Digital (AC-3) -- DTS:X / DTS-HD MA (8+ channels) -- DTS-HD MA (6 channels) +- DTS-HD MA* +- DTS-HD HRA* - DTS (DCA) - Other/PCM +*These formats can also contain spatial audio encoding such as Dolby Atmos or DTS:X on top of the base audio format + ### Video Formats - Dolby Vision - HDR10 -- HDR10+ +- HDR10+ (platform/build specific) - HLG - SDR @@ -48,7 +50,7 @@ This addon streamlines your viewing experience by automating the process of audi 1. Download the addon from the Kodi repository or install it manually. 2. Enable the addon in Kodi's addon settings. 3. Open and briefly play any video to fully initialize and enable all addon settings. -4. Configure your desired audio offsets for different HDR types, audio formats, and FPS types in the addon settings. +4. Configure your desired audio offsets for different HDR types, audio formats, and FPS types in the addon settings. Enabling FPS based offsets allows different offsets to be applied and saved based on the FPS of the source video, in addition to the HDR type and audio format, allowing for more fine-tuned control. 5. If you want to perform initial AV calibration, enable the active monitoring mode in the addon settings. This will allow the addon to learn and store your manual audio offset adjustments for future use. 6. The addon will run as a background service, automatically applying your configured offsets during playback. diff --git a/script.audiooffsetmanager/addon.xml b/script.audiooffsetmanager/addon.xml index da30a49681..b714eb91d6 100644 --- a/script.audiooffsetmanager/addon.xml +++ b/script.audiooffsetmanager/addon.xml @@ -1,5 +1,5 @@ - + @@ -7,8 +7,10 @@ https://github.com/matthane/script.audiooffsetmanager - v1.3.2 (2024-09-11) -- Seek back bug fixes + v1.4.0 (2025-01-17) +- Add DTS-HD HRA offset option +- Consolidate DTS-HD MA offset options for simplicity +- Clean up audio format detection methods to ignore spatial audio identifiers Dynamically manages audio offsets This add-on intelligently adjusts the audio offset based on the detected HDR type, audio format, and FPS type according to user-configured settings, along with other helpful features diff --git a/script.audiooffsetmanager/resources/language/resource.language.en_gb/strings.po b/script.audiooffsetmanager/resources/language/resource.language.en_gb/strings.po index 2fb431c395..2c6c310596 100644 --- a/script.audiooffsetmanager/resources/language/resource.language.en_gb/strings.po +++ b/script.audiooffsetmanager/resources/language/resource.language.en_gb/strings.po @@ -26,11 +26,11 @@ msgid "Offset Controls" msgstr "" msgctxt "#32004" -msgid "Dolby Atmos/TrueHD (ms)" +msgid "Dolby TrueHD (ms)" msgstr "" msgctxt "#32005" -msgid "Adjusts the audio offset for Dolby Atmos/TrueHD in milliseconds. Negative values delay the audio, positive values advance it." +msgid "Adjusts the audio offset for Dolby TrueHD in milliseconds. Negative values delay the audio, positive values advance it." msgstr "" msgctxt "#32006" @@ -50,19 +50,19 @@ msgid "Adjusts the audio offset for Dolby Digital (AC-3) in milliseconds. Negati msgstr "" msgctxt "#32010" -msgid "DTS:X/DTS-HD MA 8+ Channel (ms)" +msgid "DTS-HD HRA (ms)" msgstr "" msgctxt "#32011" -msgid "Adjusts the audio offset for DTS:X/DTS-HD MA 8+ Channel in milliseconds. Negative values delay the audio, positive values advance it." +msgid "Adjusts the audio offset for DTS-HD HRA in milliseconds. Negative values delay the audio, positive values advance it." msgstr "" msgctxt "#32012" -msgid "DTS-HD MA 6 Channel (ms)" +msgid "DTS-HD MA (ms)" msgstr "" msgctxt "#32013" -msgid "Adjusts the audio offset for DTS-HD MA 6 Channel in milliseconds. Negative values delay the audio, positive values advance it." +msgid "Adjusts the audio offset for DTS-HD MA in milliseconds. Negative values delay the audio, positive values advance it." msgstr "" msgctxt "#32014" @@ -306,11 +306,11 @@ msgid "60.00" msgstr "" msgctxt "#32074" -msgid "Enable FPS based offsets" +msgid "Enable content FPS-based offsets" msgstr "" msgctxt "#32075" -msgid "Control whether offsets are applied based on source FPS" +msgid "Control whether offsets are applied based on content FPS" msgstr "" msgctxt "#32076" diff --git a/script.audiooffsetmanager/resources/lib/active_monitor.py b/script.audiooffsetmanager/resources/lib/active_monitor.py index 3efe488a4a..4f7854afce 100644 --- a/script.audiooffsetmanager/resources/lib/active_monitor.py +++ b/script.audiooffsetmanager/resources/lib/active_monitor.py @@ -1,234 +1,234 @@ -"""Active monitor module to detect user changes in audio offset values during playback.""" - -import xbmc -import xbmcgui -import threading -from resources.lib.settings_manager import SettingsManager - - -class ActiveMonitor: - # Dialog IDs as constants for better maintainability - AUDIO_SETTINGS_DIALOG = 10124 - AUDIO_SLIDER_DIALOG = 10145 - - def __init__(self, event_manager, stream_info, offset_manager): - self.event_manager = event_manager - self.stream_info = stream_info - self.offset_manager = offset_manager - self.settings_manager = SettingsManager() - self.monitor_thread = None - - # Consolidated state management - self.state = { - 'monitor_active': False, - 'playback_active': False, - 'audio_settings_open': False, - 'slider_was_open': False, # Track previous slider state - 'last_audio_delay': None, - 'last_stored_delay': None, - 'last_processed_delay': None - } - - def start(self): - """Start the active monitor if it's not already running.""" - if not self.state['monitor_active']: - self._initialize_monitoring() - self._start_monitor_thread() - xbmc.log("AOM_ActiveMonitor: Active monitoring started", xbmc.LOGDEBUG) - - def stop(self): - """Stop the active monitor if it's running.""" - if self.state['monitor_active']: - self._cleanup_monitoring() - xbmc.log("AOM_ActiveMonitor: Active monitoring stopped", xbmc.LOGDEBUG) - - def _initialize_monitoring(self): - """Initialize monitoring state and update necessary information.""" - self.state.update({ - 'monitor_active': True, - 'playback_active': True, - 'audio_settings_open': False, - 'slider_was_open': False - }) - self.update_stream_info() - self.update_last_stored_audio_delay() - - def _cleanup_monitoring(self): - """Clean up monitoring state and stop the monitor thread.""" - self.state['monitor_active'] = False - self.state['playback_active'] = False - if self.monitor_thread is not None: - self.monitor_thread.join() - self.monitor_thread = None - - def _start_monitor_thread(self): - """Start the monitor thread if it's not already running.""" - if self.monitor_thread is None: - self.monitor_thread = threading.Thread(target=self.monitor_audio_offset) - self.monitor_thread.start() - - def update_stream_info(self): - """Update and validate stream information.""" - self.stream_info.update_stream_info() - xbmc.log(f"AOM_ActiveMonitor: Updated stream info: {self.stream_info.info}", - xbmc.LOGDEBUG) - - def _validate_stream_info(self): - """Validate current stream information. - - Returns: - tuple: (is_valid, stream_info_dict) or (False, None) if invalid - """ - stream_info = self.stream_info.info - required_keys = ['hdr_type', 'video_fps_type', 'audio_format'] - - if any(stream_info.get(key, 'unknown') == 'unknown' for key in required_keys): - xbmc.log(f"AOM_ActiveMonitor: Invalid stream info: {stream_info}", - xbmc.LOGDEBUG) - return False, None - - return True, stream_info - - def update_last_stored_audio_delay(self): - """Update the last stored audio delay from settings.""" - try: - is_valid, stream_info = self._validate_stream_info() - if not is_valid: - return - - setting_id = self._get_setting_id(stream_info) - self.state['last_stored_delay'] = self.settings_manager.get_setting_integer(setting_id) - self.state['last_processed_delay'] = self.state['last_stored_delay'] - - xbmc.log(f"AOM_ActiveMonitor: Updated last stored audio delay to " - f"{self.state['last_stored_delay']} for setting {setting_id}", - xbmc.LOGDEBUG) - - except Exception as e: - xbmc.log(f"AOM_ActiveMonitor: Error updating last stored audio delay: {str(e)}", - xbmc.LOGERROR) - - def _get_setting_id(self, stream_info): - """Generate setting ID from stream information.""" - return f"{stream_info['hdr_type']}_{stream_info['video_fps_type']}_{stream_info['audio_format']}" - - def convert_delay_to_ms(self, delay_str): - """Convert delay string to milliseconds integer. - - Args: - delay_str: Delay string in format '-0.075 s' - - Returns: - int: Delay in milliseconds or None if conversion fails - """ - try: - delay_seconds = float(delay_str.replace(' s', '')) - return int(delay_seconds * 1000) - except (ValueError, AttributeError): - return None - - def _handle_dialog_state(self, current_dialog_id, monitor): - """Handle dialog state changes and audio delay processing. - - Returns: - float: Wait time for next iteration - """ - # Track if slider is currently open - slider_is_open = current_dialog_id == self.AUDIO_SLIDER_DIALOG - - # Handle audio settings dialog state - if current_dialog_id == self.AUDIO_SETTINGS_DIALOG: - if not self.state['audio_settings_open']: - self.state['audio_settings_open'] = True - self.state['last_processed_delay'] = None - xbmc.log("AOM_ActiveMonitor: Audio settings opened", xbmc.LOGDEBUG) - elif self.state['audio_settings_open'] and current_dialog_id != self.AUDIO_SETTINGS_DIALOG: - self.state['audio_settings_open'] = False - xbmc.log("AOM_ActiveMonitor: Audio settings closed", xbmc.LOGDEBUG) - - # Handle slider state and updates - if slider_is_open: - self.state['slider_was_open'] = True - self._update_current_delay() - xbmc.log("AOM_ActiveMonitor: Slider is open, monitoring changes", xbmc.LOGDEBUG) - elif self.state['slider_was_open']: # Slider just closed - self.state['slider_was_open'] = False - xbmc.log("AOM_ActiveMonitor: Slider closed, processing changes", xbmc.LOGDEBUG) - self._process_final_delay() - - # Determine polling rate based on dialog states - return 0.25 if (self.state['audio_settings_open'] or slider_is_open) else 1.0 - - def _check_for_slider_dialog(self, monitor): - """Check if the audio slider dialog appears within 1 second.""" - start_time = xbmc.getGlobalIdleTime() - while (xbmc.getGlobalIdleTime() - start_time) < 1: - if xbmcgui.getCurrentWindowDialogId() == self.AUDIO_SLIDER_DIALOG: - return True - if monitor.waitForAbort(0.1): - return False - return False - - def _update_current_delay(self): - """Update the current audio delay value.""" - current_delay = xbmc.getInfoLabel('Player.AudioDelay') - if current_delay != self.state['last_audio_delay']: - self.state['last_audio_delay'] = current_delay - xbmc.log(f"AOM_ActiveMonitor: Current delay updated to {current_delay}", - xbmc.LOGDEBUG) - - def _process_final_delay(self): - """Process the final audio delay value when slider closes.""" - current_delay_ms = self.convert_delay_to_ms(self.state['last_audio_delay']) - if (current_delay_ms is not None and - current_delay_ms != self.state['last_processed_delay']): - xbmc.log("AOM_ActiveMonitor: Processing delay change after slider close", - xbmc.LOGDEBUG) - self.process_audio_delay_change(self.state['last_audio_delay']) - self.state['last_processed_delay'] = current_delay_ms - - def monitor_audio_offset(self): - """Main monitoring loop for audio offset changes.""" - monitor = xbmc.Monitor() - - while (self.state['monitor_active'] and - self.state['playback_active'] and - not monitor.abortRequested()): - - current_dialog_id = xbmcgui.getCurrentWindowDialogId() - wait_time = self._handle_dialog_state(current_dialog_id, monitor) - - if monitor.waitForAbort(wait_time): - break - - def process_audio_delay_change(self, audio_delay): - """Process and store audio delay changes. - - Args: - audio_delay: The new audio delay value to process - """ - try: - xbmc.log(f"AOM_ActiveMonitor: Processing final audio delay: {audio_delay}", - xbmc.LOGDEBUG) - - delay_ms = self.convert_delay_to_ms(audio_delay) - if delay_ms is None: - return - - is_valid, stream_info = self._validate_stream_info() - if not is_valid: - return - - setting_id = self._get_setting_id(stream_info) - current_delay_ms = self.settings_manager.get_setting_integer(setting_id) - - if delay_ms != current_delay_ms: - self.settings_manager.store_setting_integer(setting_id, delay_ms) - xbmc.log(f"AOM_ActiveMonitor: Stored audio offset {delay_ms}ms " - f"for setting {setting_id}", xbmc.LOGDEBUG) - self.event_manager.publish('USER_ADJUSTMENT') - self.state['last_stored_delay'] = delay_ms - - except Exception as e: - xbmc.log(f"AOM_ActiveMonitor: Error processing audio delay change: {str(e)}", - xbmc.LOGERROR) +"""Active monitor module to detect user changes in audio offset values during playback.""" + +import xbmc +import xbmcgui +import threading +from resources.lib.settings_manager import SettingsManager + + +class ActiveMonitor: + # Dialog IDs as constants for better maintainability + AUDIO_SETTINGS_DIALOG = 10124 + AUDIO_SLIDER_DIALOG = 10145 + + def __init__(self, event_manager, stream_info, offset_manager): + self.event_manager = event_manager + self.stream_info = stream_info + self.offset_manager = offset_manager + self.settings_manager = SettingsManager() + self.monitor_thread = None + + # Consolidated state management + self.state = { + 'monitor_active': False, + 'playback_active': False, + 'audio_settings_open': False, + 'slider_was_open': False, # Track previous slider state + 'last_audio_delay': None, + 'last_stored_delay': None, + 'last_processed_delay': None + } + + def start(self): + """Start the active monitor if it's not already running.""" + if not self.state['monitor_active']: + self._initialize_monitoring() + self._start_monitor_thread() + xbmc.log("AOM_ActiveMonitor: Active monitoring started", xbmc.LOGDEBUG) + + def stop(self): + """Stop the active monitor if it's running.""" + if self.state['monitor_active']: + self._cleanup_monitoring() + xbmc.log("AOM_ActiveMonitor: Active monitoring stopped", xbmc.LOGDEBUG) + + def _initialize_monitoring(self): + """Initialize monitoring state and update necessary information.""" + self.state.update({ + 'monitor_active': True, + 'playback_active': True, + 'audio_settings_open': False, + 'slider_was_open': False + }) + self.update_stream_info() + self.update_last_stored_audio_delay() + + def _cleanup_monitoring(self): + """Clean up monitoring state and stop the monitor thread.""" + self.state['monitor_active'] = False + self.state['playback_active'] = False + if self.monitor_thread is not None: + self.monitor_thread.join() + self.monitor_thread = None + + def _start_monitor_thread(self): + """Start the monitor thread if it's not already running.""" + if self.monitor_thread is None: + self.monitor_thread = threading.Thread(target=self.monitor_audio_offset) + self.monitor_thread.start() + + def update_stream_info(self): + """Update and validate stream information.""" + self.stream_info.update_stream_info() + xbmc.log(f"AOM_ActiveMonitor: Updated stream info: {self.stream_info.info}", + xbmc.LOGDEBUG) + + def _validate_stream_info(self): + """Validate current stream information. + + Returns: + tuple: (is_valid, stream_info_dict) or (False, None) if invalid + """ + stream_info = self.stream_info.info + required_keys = ['hdr_type', 'video_fps_type', 'audio_format'] + + if any(stream_info.get(key, 'unknown') == 'unknown' for key in required_keys): + xbmc.log(f"AOM_ActiveMonitor: Invalid stream info: {stream_info}", + xbmc.LOGDEBUG) + return False, None + + return True, stream_info + + def update_last_stored_audio_delay(self): + """Update the last stored audio delay from settings.""" + try: + is_valid, stream_info = self._validate_stream_info() + if not is_valid: + return + + setting_id = self._get_setting_id(stream_info) + self.state['last_stored_delay'] = self.settings_manager.get_setting_integer(setting_id) + self.state['last_processed_delay'] = self.state['last_stored_delay'] + + xbmc.log(f"AOM_ActiveMonitor: Updated last stored audio delay to " + f"{self.state['last_stored_delay']} for setting {setting_id}", + xbmc.LOGDEBUG) + + except Exception as e: + xbmc.log(f"AOM_ActiveMonitor: Error updating last stored audio delay: {str(e)}", + xbmc.LOGERROR) + + def _get_setting_id(self, stream_info): + """Generate setting ID from stream information.""" + return f"{stream_info['hdr_type']}_{stream_info['video_fps_type']}_{stream_info['audio_format']}" + + def convert_delay_to_ms(self, delay_str): + """Convert delay string to milliseconds integer. + + Args: + delay_str: Delay string in format '-0.075 s' + + Returns: + int: Delay in milliseconds or None if conversion fails + """ + try: + delay_seconds = float(delay_str.replace(' s', '')) + return int(delay_seconds * 1000) + except (ValueError, AttributeError): + return None + + def _handle_dialog_state(self, current_dialog_id, monitor): + """Handle dialog state changes and audio delay processing. + + Returns: + float: Wait time for next iteration + """ + # Track if slider is currently open + slider_is_open = current_dialog_id == self.AUDIO_SLIDER_DIALOG + + # Handle audio settings dialog state + if current_dialog_id == self.AUDIO_SETTINGS_DIALOG: + if not self.state['audio_settings_open']: + self.state['audio_settings_open'] = True + self.state['last_processed_delay'] = None + xbmc.log("AOM_ActiveMonitor: Audio settings opened", xbmc.LOGDEBUG) + elif self.state['audio_settings_open'] and current_dialog_id != self.AUDIO_SETTINGS_DIALOG: + self.state['audio_settings_open'] = False + xbmc.log("AOM_ActiveMonitor: Audio settings closed", xbmc.LOGDEBUG) + + # Handle slider state and updates + if slider_is_open: + self.state['slider_was_open'] = True + self._update_current_delay() + xbmc.log("AOM_ActiveMonitor: Slider is open, monitoring changes", xbmc.LOGDEBUG) + elif self.state['slider_was_open']: # Slider just closed + self.state['slider_was_open'] = False + xbmc.log("AOM_ActiveMonitor: Slider closed, processing changes", xbmc.LOGDEBUG) + self._process_final_delay() + + # Determine polling rate based on dialog states + return 0.25 if (self.state['audio_settings_open'] or slider_is_open) else 1.0 + + def _check_for_slider_dialog(self, monitor): + """Check if the audio slider dialog appears within 1 second.""" + start_time = xbmc.getGlobalIdleTime() + while (xbmc.getGlobalIdleTime() - start_time) < 1: + if xbmcgui.getCurrentWindowDialogId() == self.AUDIO_SLIDER_DIALOG: + return True + if monitor.waitForAbort(0.1): + return False + return False + + def _update_current_delay(self): + """Update the current audio delay value.""" + current_delay = xbmc.getInfoLabel('Player.AudioDelay') + if current_delay != self.state['last_audio_delay']: + self.state['last_audio_delay'] = current_delay + xbmc.log(f"AOM_ActiveMonitor: Current delay updated to {current_delay}", + xbmc.LOGDEBUG) + + def _process_final_delay(self): + """Process the final audio delay value when slider closes.""" + current_delay_ms = self.convert_delay_to_ms(self.state['last_audio_delay']) + if (current_delay_ms is not None and + current_delay_ms != self.state['last_processed_delay']): + xbmc.log("AOM_ActiveMonitor: Processing delay change after slider close", + xbmc.LOGDEBUG) + self.process_audio_delay_change(self.state['last_audio_delay']) + self.state['last_processed_delay'] = current_delay_ms + + def monitor_audio_offset(self): + """Main monitoring loop for audio offset changes.""" + monitor = xbmc.Monitor() + + while (self.state['monitor_active'] and + self.state['playback_active'] and + not monitor.abortRequested()): + + current_dialog_id = xbmcgui.getCurrentWindowDialogId() + wait_time = self._handle_dialog_state(current_dialog_id, monitor) + + if monitor.waitForAbort(wait_time): + break + + def process_audio_delay_change(self, audio_delay): + """Process and store audio delay changes. + + Args: + audio_delay: The new audio delay value to process + """ + try: + xbmc.log(f"AOM_ActiveMonitor: Processing final audio delay: {audio_delay}", + xbmc.LOGDEBUG) + + delay_ms = self.convert_delay_to_ms(audio_delay) + if delay_ms is None: + return + + is_valid, stream_info = self._validate_stream_info() + if not is_valid: + return + + setting_id = self._get_setting_id(stream_info) + current_delay_ms = self.settings_manager.get_setting_integer(setting_id) + + if delay_ms != current_delay_ms: + self.settings_manager.store_setting_integer(setting_id, delay_ms) + xbmc.log(f"AOM_ActiveMonitor: Stored audio offset {delay_ms}ms " + f"for setting {setting_id}", xbmc.LOGDEBUG) + self.event_manager.publish('USER_ADJUSTMENT') + self.state['last_stored_delay'] = delay_ms + + except Exception as e: + xbmc.log(f"AOM_ActiveMonitor: Error processing audio delay change: {str(e)}", + xbmc.LOGERROR) diff --git a/script.audiooffsetmanager/resources/lib/offset_manager.py b/script.audiooffsetmanager/resources/lib/offset_manager.py index ad936b9069..d3ebf2f7ad 100644 --- a/script.audiooffsetmanager/resources/lib/offset_manager.py +++ b/script.audiooffsetmanager/resources/lib/offset_manager.py @@ -1,176 +1,176 @@ -"""Offset manager module to receive playback events and assign audio offsets as needed. -This module also controls the deployment of the Active Monitor when it's enabled. -""" - -import xbmc -import json -from resources.lib.settings_manager import SettingsManager -from resources.lib.stream_info import StreamInfo -from resources.lib.active_monitor import ActiveMonitor - - -class OffsetManager: - def __init__(self, event_manager): - self.event_manager = event_manager - self.stream_info = StreamInfo() - self.settings_manager = SettingsManager() - self.active_monitor = None - - def start(self): - """Start the offset manager by subscribing to relevant events.""" - events = { - 'AV_STARTED': self.on_av_started, - 'ON_AV_CHANGE': self.on_av_change, - 'PLAYBACK_STOPPED': self.on_playback_stopped, - 'PLAYBACK_ENDED': self.on_playback_stopped - } - for event, callback in events.items(): - self.event_manager.subscribe(event, callback) - - def stop(self): - """Stop the offset manager and clean up subscriptions.""" - events = { - 'AV_STARTED': self.on_av_started, - 'ON_AV_CHANGE': self.on_av_change, - 'PLAYBACK_STOPPED': self.on_playback_stopped, - 'PLAYBACK_ENDED': self.on_playback_stopped - } - for event, callback in events.items(): - self.event_manager.unsubscribe(event, callback) - self.stop_active_monitor() - - def on_av_started(self): - """Handle AV started event.""" - self._handle_av_event() - - def on_av_change(self): - """Handle AV change event.""" - self._handle_av_event() - - def on_playback_stopped(self): - """Handle playback stopped event.""" - self.stream_info.clear_stream_info() - self.stop_active_monitor() - - def _handle_av_event(self): - """Common handler for AV-related events.""" - self.stream_info.update_stream_info() - self.apply_audio_offset() - self.manage_active_monitor() - - def _should_apply_offset(self): - """Check if audio offset should be applied based on current conditions.""" - if self.settings_manager.get_setting_boolean('new_install'): - xbmc.log("AOM_OffsetManager: New install detected. Skipping " - "audio offset application.", xbmc.LOGDEBUG) - return False - - stream_info = self.stream_info.info - # Check for unknown formats - if any(stream_info[key] == 'unknown' for key in - ['hdr_type', 'audio_format', 'video_fps_type']): - xbmc.log(f"AOM_OffsetManager: Skipping audio offset - Unknown format detected " - f"(HDR: {stream_info['hdr_type']}, Audio: {stream_info['audio_format']}, " - f"FPS: {stream_info['video_fps_type']})", xbmc.LOGDEBUG) - return False - - # Check if HDR type is enabled - if not self.settings_manager.get_setting_boolean(f"enable_{stream_info['hdr_type']}"): - xbmc.log(f"AOM_OffsetManager: HDR type {stream_info['hdr_type']} is not " - f"enabled in settings", xbmc.LOGDEBUG) - return False - - return True - - def _get_setting_id(self, stream_info): - """Generate the setting ID for the current stream configuration.""" - return f"{stream_info['hdr_type']}_{stream_info['video_fps_type']}_{stream_info['audio_format']}" - - def apply_audio_offset(self): - """Apply audio offset based on current stream information and settings.""" - try: - if not self._should_apply_offset(): - return - - stream_info = self.stream_info.info - setting_id = self._get_setting_id(stream_info) - delay_ms = self.settings_manager.get_setting_integer(setting_id) - - if delay_ms is None: - xbmc.log(f"AOM_OffsetManager: No audio delay found for setting ID: {setting_id}", - xbmc.LOGDEBUG) - return - - if stream_info['player_id'] != -1: - self.set_audio_delay(stream_info['player_id'], delay_ms / 1000.0) - else: - xbmc.log("AOM_OffsetManager: No valid player ID found to set " - "audio delay", xbmc.LOGDEBUG) - - except Exception as e: - xbmc.log(f"AOM_OffsetManager: Error applying audio offset: {str(e)}", - xbmc.LOGERROR) - - def set_audio_delay(self, player_id, delay_seconds): - """Set the audio delay using JSON-RPC.""" - try: - request = { - "jsonrpc": "2.0", - "method": "Player.SetAudioDelay", - "params": { - "playerid": player_id, - "offset": delay_seconds - }, - "id": 1 - } - response = xbmc.executeJSONRPC(json.dumps(request)) - response_json = json.loads(response) - - if "error" in response_json: - xbmc.log(f"AOM_OffsetManager: Failed to set audio offset: " - f"{response_json['error']}", xbmc.LOGWARNING) - else: - xbmc.log(f"AOM_OffsetManager: Audio offset set to " - f"{delay_seconds} seconds", xbmc.LOGDEBUG) - except Exception as e: - xbmc.log(f"AOM_OffsetManager: Error setting audio delay: {str(e)}", - xbmc.LOGERROR) - - def _should_start_active_monitor(self): - """Determine if active monitor should be started based on current conditions.""" - stream_info = self.stream_info.info - active_monitoring_enabled = self.settings_manager.get_setting_boolean('enable_active_monitoring') - hdr_type = stream_info['hdr_type'] - fps_type = stream_info['video_fps_type'] - hdr_type_enabled = self.settings_manager.get_setting_boolean(f'enable_{hdr_type}') - - return (active_monitoring_enabled and - hdr_type_enabled and - hdr_type != 'unknown' and - fps_type != 'unknown') - - def manage_active_monitor(self): - """Manage the active monitor state based on current conditions.""" - xbmc.log(f"AOM_OffsetManager: Checking active monitor status - " - f"HDR: {self.stream_info.info['hdr_type']}, " - f"FPS: {self.stream_info.info['video_fps_type']}", - xbmc.LOGDEBUG) - - if self._should_start_active_monitor(): - self.start_active_monitor() - else: - self.stop_active_monitor() - - def start_active_monitor(self): - """Start the active monitor if it's not already running.""" - if self.active_monitor is None: - self.active_monitor = ActiveMonitor(self.event_manager, self.stream_info, self) - self.active_monitor.start() - xbmc.log("AOM_OffsetManager: Active monitor started", xbmc.LOGDEBUG) - - def stop_active_monitor(self): - """Stop the active monitor if it's running.""" - if self.active_monitor is not None: - self.active_monitor.stop() - self.active_monitor = None - xbmc.log("AOM_OffsetManager: Active monitor stopped", xbmc.LOGDEBUG) +"""Offset manager module to receive playback events and assign audio offsets as needed. +This module also controls the deployment of the Active Monitor when it's enabled. +""" + +import xbmc +import json +from resources.lib.settings_manager import SettingsManager +from resources.lib.stream_info import StreamInfo +from resources.lib.active_monitor import ActiveMonitor + + +class OffsetManager: + def __init__(self, event_manager): + self.event_manager = event_manager + self.stream_info = StreamInfo() + self.settings_manager = SettingsManager() + self.active_monitor = None + + def start(self): + """Start the offset manager by subscribing to relevant events.""" + events = { + 'AV_STARTED': self.on_av_started, + 'ON_AV_CHANGE': self.on_av_change, + 'PLAYBACK_STOPPED': self.on_playback_stopped, + 'PLAYBACK_ENDED': self.on_playback_stopped + } + for event, callback in events.items(): + self.event_manager.subscribe(event, callback) + + def stop(self): + """Stop the offset manager and clean up subscriptions.""" + events = { + 'AV_STARTED': self.on_av_started, + 'ON_AV_CHANGE': self.on_av_change, + 'PLAYBACK_STOPPED': self.on_playback_stopped, + 'PLAYBACK_ENDED': self.on_playback_stopped + } + for event, callback in events.items(): + self.event_manager.unsubscribe(event, callback) + self.stop_active_monitor() + + def on_av_started(self): + """Handle AV started event.""" + self._handle_av_event() + + def on_av_change(self): + """Handle AV change event.""" + self._handle_av_event() + + def on_playback_stopped(self): + """Handle playback stopped event.""" + self.stream_info.clear_stream_info() + self.stop_active_monitor() + + def _handle_av_event(self): + """Common handler for AV-related events.""" + self.stream_info.update_stream_info() + self.apply_audio_offset() + self.manage_active_monitor() + + def _should_apply_offset(self): + """Check if audio offset should be applied based on current conditions.""" + if self.settings_manager.get_setting_boolean('new_install'): + xbmc.log("AOM_OffsetManager: New install detected. Skipping " + "audio offset application.", xbmc.LOGDEBUG) + return False + + stream_info = self.stream_info.info + # Check for unknown formats + if any(stream_info[key] == 'unknown' for key in + ['hdr_type', 'audio_format', 'video_fps_type']): + xbmc.log(f"AOM_OffsetManager: Skipping audio offset - Unknown format detected " + f"(HDR: {stream_info['hdr_type']}, Audio: {stream_info['audio_format']}, " + f"FPS: {stream_info['video_fps_type']})", xbmc.LOGDEBUG) + return False + + # Check if HDR type is enabled + if not self.settings_manager.get_setting_boolean(f"enable_{stream_info['hdr_type']}"): + xbmc.log(f"AOM_OffsetManager: HDR type {stream_info['hdr_type']} is not " + f"enabled in settings", xbmc.LOGDEBUG) + return False + + return True + + def _get_setting_id(self, stream_info): + """Generate the setting ID for the current stream configuration.""" + return f"{stream_info['hdr_type']}_{stream_info['video_fps_type']}_{stream_info['audio_format']}" + + def apply_audio_offset(self): + """Apply audio offset based on current stream information and settings.""" + try: + if not self._should_apply_offset(): + return + + stream_info = self.stream_info.info + setting_id = self._get_setting_id(stream_info) + delay_ms = self.settings_manager.get_setting_integer(setting_id) + + if delay_ms is None: + xbmc.log(f"AOM_OffsetManager: No audio delay found for setting ID: {setting_id}", + xbmc.LOGDEBUG) + return + + if stream_info['player_id'] != -1: + self.set_audio_delay(stream_info['player_id'], delay_ms / 1000.0) + else: + xbmc.log("AOM_OffsetManager: No valid player ID found to set " + "audio delay", xbmc.LOGDEBUG) + + except Exception as e: + xbmc.log(f"AOM_OffsetManager: Error applying audio offset: {str(e)}", + xbmc.LOGERROR) + + def set_audio_delay(self, player_id, delay_seconds): + """Set the audio delay using JSON-RPC.""" + try: + request = { + "jsonrpc": "2.0", + "method": "Player.SetAudioDelay", + "params": { + "playerid": player_id, + "offset": delay_seconds + }, + "id": 1 + } + response = xbmc.executeJSONRPC(json.dumps(request)) + response_json = json.loads(response) + + if "error" in response_json: + xbmc.log(f"AOM_OffsetManager: Failed to set audio offset: " + f"{response_json['error']}", xbmc.LOGWARNING) + else: + xbmc.log(f"AOM_OffsetManager: Audio offset set to " + f"{delay_seconds} seconds", xbmc.LOGDEBUG) + except Exception as e: + xbmc.log(f"AOM_OffsetManager: Error setting audio delay: {str(e)}", + xbmc.LOGERROR) + + def _should_start_active_monitor(self): + """Determine if active monitor should be started based on current conditions.""" + stream_info = self.stream_info.info + active_monitoring_enabled = self.settings_manager.get_setting_boolean('enable_active_monitoring') + hdr_type = stream_info['hdr_type'] + fps_type = stream_info['video_fps_type'] + hdr_type_enabled = self.settings_manager.get_setting_boolean(f'enable_{hdr_type}') + + return (active_monitoring_enabled and + hdr_type_enabled and + hdr_type != 'unknown' and + fps_type != 'unknown') + + def manage_active_monitor(self): + """Manage the active monitor state based on current conditions.""" + xbmc.log(f"AOM_OffsetManager: Checking active monitor status - " + f"HDR: {self.stream_info.info['hdr_type']}, " + f"FPS: {self.stream_info.info['video_fps_type']}", + xbmc.LOGDEBUG) + + if self._should_start_active_monitor(): + self.start_active_monitor() + else: + self.stop_active_monitor() + + def start_active_monitor(self): + """Start the active monitor if it's not already running.""" + if self.active_monitor is None: + self.active_monitor = ActiveMonitor(self.event_manager, self.stream_info, self) + self.active_monitor.start() + xbmc.log("AOM_OffsetManager: Active monitor started", xbmc.LOGDEBUG) + + def stop_active_monitor(self): + """Stop the active monitor if it's running.""" + if self.active_monitor is not None: + self.active_monitor.stop() + self.active_monitor = None + xbmc.log("AOM_OffsetManager: Active monitor stopped", xbmc.LOGDEBUG) diff --git a/script.audiooffsetmanager/resources/lib/seek_backs.py b/script.audiooffsetmanager/resources/lib/seek_backs.py index b86cfb7964..09c6ef6389 100644 --- a/script.audiooffsetmanager/resources/lib/seek_backs.py +++ b/script.audiooffsetmanager/resources/lib/seek_backs.py @@ -1,206 +1,206 @@ -"""Seek backs module submits player seek commands based on playback events.""" - -import xbmc -import json -import time -from resources.lib.settings_manager import SettingsManager - - -class SeekBacks: - # Event type mapping for settings - SETTING_TYPE_MAP = { - 'resume': 'resume', - 'adjust': 'adjust', - 'unpause': 'unpause', - 'change': 'change' # Keep 'change' separate from 'adjust' - } - - def __init__(self, event_manager): - self.event_manager = event_manager - self.settings_manager = SettingsManager() - self.playback_state = { - 'paused': False, - 'last_seek_time': 0 # Track the last time we performed a seek - } - - def start(self): - """Start the seek backs module by subscribing to relevant events.""" - events = { - 'AV_STARTED': self.on_av_started, - 'ON_AV_CHANGE': self.on_av_change, - 'PLAYBACK_RESUMED': self.on_av_unpause, - 'PLAYBACK_PAUSED': self.on_playback_paused, - 'USER_ADJUSTMENT': self.on_user_adjustment, - 'PLAYBACK_STOPPED': self.on_playback_stopped, - 'PLAYBACK_ENDED': self.on_playback_stopped # Use same handler for both stop and end - } - for event, callback in events.items(): - self.event_manager.subscribe(event, callback) - - def stop(self): - """Stop the seek backs module and clean up subscriptions.""" - events = { - 'AV_STARTED': self.on_av_started, - 'ON_AV_CHANGE': self.on_av_change, - 'PLAYBACK_RESUMED': self.on_av_unpause, - 'PLAYBACK_PAUSED': self.on_playback_paused, - 'USER_ADJUSTMENT': self.on_user_adjustment, - 'PLAYBACK_STOPPED': self.on_playback_stopped, - 'PLAYBACK_ENDED': self.on_playback_stopped - } - for event, callback in events.items(): - self.event_manager.unsubscribe(event, callback) - - def on_av_started(self): - """Handle AV started event.""" - # Reset playback state when new playback starts - self.playback_state['paused'] = False - self.perform_seek_back('resume') - - def on_av_change(self): - """Handle AV change event.""" - self.perform_seek_back('adjust') - - def on_av_unpause(self): - """Handle playback resume event.""" - xbmc.sleep(500) # Small delay to avoid race condition on flag - self.playback_state['paused'] = False - self.perform_seek_back('unpause') - - def on_playback_paused(self): - """Handle playback paused event.""" - self.playback_state['paused'] = True - - def on_playback_stopped(self): - """Handle playback stopped/ended event.""" - xbmc.log("AOM_SeekBacks: Playback stopped/ended, resetting playback state", xbmc.LOGDEBUG) - self.playback_state['paused'] = False - self.playback_state['last_seek_time'] = 0 - - def on_user_adjustment(self): - """Handle user adjustment event.""" - xbmc.log("AOM_SeekBacks: Processing user adjustment event", xbmc.LOGDEBUG) - # Check if seek back is enabled for changes (user adjustments) - if self.settings_manager.get_setting_boolean('enable_seek_back_change'): - self.perform_seek_back('change') - else: - xbmc.log("AOM_SeekBacks: Seek back for user adjustments is disabled", xbmc.LOGDEBUG) - - def _get_setting_type(self, event_type): - """Get the correct setting type based on event type. - - Args: - event_type: The type of event triggering the seek back - - Returns: - str: The corresponding setting type - """ - return self.SETTING_TYPE_MAP.get(event_type, event_type) - - def _should_perform_seek_back(self, event_type): - """Check if seek back should be performed based on current conditions. - - Args: - event_type: The type of event triggering the seek back - - Returns: - tuple: (should_seek, seek_seconds) or (False, None) if seek is not needed - """ - # Check if we've performed a seek back recently (within 2 seconds) - current_time = time.time() - if current_time - self.playback_state['last_seek_time'] < 2: - xbmc.log(f"AOM_SeekBacks: Skipping seek back on {event_type} - too soon after last seek", - xbmc.LOGDEBUG) - return False, None - - if self.playback_state['paused']: - xbmc.log(f"AOM_SeekBacks: Playback is paused, skipping seek back " - f"on {event_type}", xbmc.LOGDEBUG) - return False, None - - setting_type = self._get_setting_type(event_type) - setting_base = f'seek_back_{setting_type}' - - # Check if seek back is enabled for this type - enable_setting = f'enable_{setting_base}' - if not self.settings_manager.get_setting_boolean(enable_setting): - xbmc.log(f"AOM_SeekBacks: Seek back on {event_type} (setting: {enable_setting}) " - f"is not enabled", xbmc.LOGDEBUG) - return False, None - - # Get seek back seconds - seconds_setting = f'{setting_base}_seconds' - seek_seconds = self.settings_manager.get_setting_integer(seconds_setting) - if seek_seconds <= 0: - xbmc.log(f"AOM_SeekBacks: Invalid seek back seconds ({seek_seconds}) " - f"for {event_type}", xbmc.LOGWARNING) - return False, None - - xbmc.log(f"AOM_SeekBacks: Will seek back {seek_seconds} seconds on {event_type} " - f"(setting: {setting_type})", xbmc.LOGDEBUG) - return True, seek_seconds - - def _execute_seek_command(self, seconds, event_type): - """Execute the JSON-RPC seek command. - - Args: - seconds: Number of seconds to seek back - event_type: The type of event that triggered the seek - - Returns: - bool: True if seek was successful, False otherwise - """ - request = { - "jsonrpc": "2.0", - "method": "Player.Seek", - "params": { - "playerid": 1, - "value": {"seconds": -seconds} - }, - "id": 1 - } - - try: - xbmc.log(f"AOM_SeekBacks: Attempting to seek back {seconds} seconds " - f"on {event_type}", xbmc.LOGDEBUG) - response = xbmc.executeJSONRPC(json.dumps(request)) - response_json = json.loads(response) - - if "error" in response_json: - xbmc.log(f"AOM_SeekBacks: Failed to perform seek back: " - f"{response_json['error']}", xbmc.LOGWARNING) - return False - - # Update last seek time only on successful seek - self.playback_state['last_seek_time'] = time.time() - xbmc.log(f"AOM_SeekBacks: Successfully seeked back by {seconds} seconds " - f"on {event_type}", xbmc.LOGDEBUG) - return True - - except Exception as e: - xbmc.log(f"AOM_SeekBacks: Error executing seek command: {str(e)}", - xbmc.LOGERROR) - return False - - def perform_seek_back(self, event_type): - """Perform seek back operation based on event type and current conditions. - - Args: - event_type: The type of event triggering the seek back - """ - try: - should_seek, seek_seconds = self._should_perform_seek_back(event_type) - - if not should_seek: - return - - # Required delay for stream settling - xbmc.sleep(2000) - - if not self._execute_seek_command(seek_seconds, event_type): - xbmc.log(f"AOM_SeekBacks: Seek back operation failed for {event_type}", - xbmc.LOGWARNING) - - except Exception as e: - xbmc.log(f"AOM_SeekBacks: Error in perform_seek_back: {str(e)}", - xbmc.LOGERROR) +"""Seek backs module submits player seek commands based on playback events.""" + +import xbmc +import json +import time +from resources.lib.settings_manager import SettingsManager + + +class SeekBacks: + # Event type mapping for settings + SETTING_TYPE_MAP = { + 'resume': 'resume', + 'adjust': 'adjust', + 'unpause': 'unpause', + 'change': 'change' # Keep 'change' separate from 'adjust' + } + + def __init__(self, event_manager): + self.event_manager = event_manager + self.settings_manager = SettingsManager() + self.playback_state = { + 'paused': False, + 'last_seek_time': 0 # Track the last time we performed a seek + } + + def start(self): + """Start the seek backs module by subscribing to relevant events.""" + events = { + 'AV_STARTED': self.on_av_started, + 'ON_AV_CHANGE': self.on_av_change, + 'PLAYBACK_RESUMED': self.on_av_unpause, + 'PLAYBACK_PAUSED': self.on_playback_paused, + 'USER_ADJUSTMENT': self.on_user_adjustment, + 'PLAYBACK_STOPPED': self.on_playback_stopped, + 'PLAYBACK_ENDED': self.on_playback_stopped # Use same handler for both stop and end + } + for event, callback in events.items(): + self.event_manager.subscribe(event, callback) + + def stop(self): + """Stop the seek backs module and clean up subscriptions.""" + events = { + 'AV_STARTED': self.on_av_started, + 'ON_AV_CHANGE': self.on_av_change, + 'PLAYBACK_RESUMED': self.on_av_unpause, + 'PLAYBACK_PAUSED': self.on_playback_paused, + 'USER_ADJUSTMENT': self.on_user_adjustment, + 'PLAYBACK_STOPPED': self.on_playback_stopped, + 'PLAYBACK_ENDED': self.on_playback_stopped + } + for event, callback in events.items(): + self.event_manager.unsubscribe(event, callback) + + def on_av_started(self): + """Handle AV started event.""" + # Reset playback state when new playback starts + self.playback_state['paused'] = False + self.perform_seek_back('resume') + + def on_av_change(self): + """Handle AV change event.""" + self.perform_seek_back('adjust') + + def on_av_unpause(self): + """Handle playback resume event.""" + xbmc.sleep(500) # Small delay to avoid race condition on flag + self.playback_state['paused'] = False + self.perform_seek_back('unpause') + + def on_playback_paused(self): + """Handle playback paused event.""" + self.playback_state['paused'] = True + + def on_playback_stopped(self): + """Handle playback stopped/ended event.""" + xbmc.log("AOM_SeekBacks: Playback stopped/ended, resetting playback state", xbmc.LOGDEBUG) + self.playback_state['paused'] = False + self.playback_state['last_seek_time'] = 0 + + def on_user_adjustment(self): + """Handle user adjustment event.""" + xbmc.log("AOM_SeekBacks: Processing user adjustment event", xbmc.LOGDEBUG) + # Check if seek back is enabled for changes (user adjustments) + if self.settings_manager.get_setting_boolean('enable_seek_back_change'): + self.perform_seek_back('change') + else: + xbmc.log("AOM_SeekBacks: Seek back for user adjustments is disabled", xbmc.LOGDEBUG) + + def _get_setting_type(self, event_type): + """Get the correct setting type based on event type. + + Args: + event_type: The type of event triggering the seek back + + Returns: + str: The corresponding setting type + """ + return self.SETTING_TYPE_MAP.get(event_type, event_type) + + def _should_perform_seek_back(self, event_type): + """Check if seek back should be performed based on current conditions. + + Args: + event_type: The type of event triggering the seek back + + Returns: + tuple: (should_seek, seek_seconds) or (False, None) if seek is not needed + """ + # Check if we've performed a seek back recently (within 2 seconds) + current_time = time.time() + if current_time - self.playback_state['last_seek_time'] < 2: + xbmc.log(f"AOM_SeekBacks: Skipping seek back on {event_type} - too soon after last seek", + xbmc.LOGDEBUG) + return False, None + + if self.playback_state['paused']: + xbmc.log(f"AOM_SeekBacks: Playback is paused, skipping seek back " + f"on {event_type}", xbmc.LOGDEBUG) + return False, None + + setting_type = self._get_setting_type(event_type) + setting_base = f'seek_back_{setting_type}' + + # Check if seek back is enabled for this type + enable_setting = f'enable_{setting_base}' + if not self.settings_manager.get_setting_boolean(enable_setting): + xbmc.log(f"AOM_SeekBacks: Seek back on {event_type} (setting: {enable_setting}) " + f"is not enabled", xbmc.LOGDEBUG) + return False, None + + # Get seek back seconds + seconds_setting = f'{setting_base}_seconds' + seek_seconds = self.settings_manager.get_setting_integer(seconds_setting) + if seek_seconds <= 0: + xbmc.log(f"AOM_SeekBacks: Invalid seek back seconds ({seek_seconds}) " + f"for {event_type}", xbmc.LOGWARNING) + return False, None + + xbmc.log(f"AOM_SeekBacks: Will seek back {seek_seconds} seconds on {event_type} " + f"(setting: {setting_type})", xbmc.LOGDEBUG) + return True, seek_seconds + + def _execute_seek_command(self, seconds, event_type): + """Execute the JSON-RPC seek command. + + Args: + seconds: Number of seconds to seek back + event_type: The type of event that triggered the seek + + Returns: + bool: True if seek was successful, False otherwise + """ + request = { + "jsonrpc": "2.0", + "method": "Player.Seek", + "params": { + "playerid": 1, + "value": {"seconds": -seconds} + }, + "id": 1 + } + + try: + xbmc.log(f"AOM_SeekBacks: Attempting to seek back {seconds} seconds " + f"on {event_type}", xbmc.LOGDEBUG) + response = xbmc.executeJSONRPC(json.dumps(request)) + response_json = json.loads(response) + + if "error" in response_json: + xbmc.log(f"AOM_SeekBacks: Failed to perform seek back: " + f"{response_json['error']}", xbmc.LOGWARNING) + return False + + # Update last seek time only on successful seek + self.playback_state['last_seek_time'] = time.time() + xbmc.log(f"AOM_SeekBacks: Successfully seeked back by {seconds} seconds " + f"on {event_type}", xbmc.LOGDEBUG) + return True + + except Exception as e: + xbmc.log(f"AOM_SeekBacks: Error executing seek command: {str(e)}", + xbmc.LOGERROR) + return False + + def perform_seek_back(self, event_type): + """Perform seek back operation based on event type and current conditions. + + Args: + event_type: The type of event triggering the seek back + """ + try: + should_seek, seek_seconds = self._should_perform_seek_back(event_type) + + if not should_seek: + return + + # Required delay for stream settling + xbmc.sleep(2000) + + if not self._execute_seek_command(seek_seconds, event_type): + xbmc.log(f"AOM_SeekBacks: Seek back operation failed for {event_type}", + xbmc.LOGWARNING) + + except Exception as e: + xbmc.log(f"AOM_SeekBacks: Error in perform_seek_back: {str(e)}", + xbmc.LOGERROR) diff --git a/script.audiooffsetmanager/resources/lib/settings_manager.py b/script.audiooffsetmanager/resources/lib/settings_manager.py index e18b12474a..4e26395919 100644 --- a/script.audiooffsetmanager/resources/lib/settings_manager.py +++ b/script.audiooffsetmanager/resources/lib/settings_manager.py @@ -1,116 +1,116 @@ -"""Settings manager module to provide methods for other modules to interface with -the addon settings. -""" - -import xbmc -import xbmcaddon - - -class SettingsManager: - _instance = None - _settings = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super(SettingsManager, cls).__new__(cls) - cls._instance._initialize() - return cls._instance - - def _initialize(self): - """Initialize the settings manager with addon settings.""" - self.addon = xbmcaddon.Addon('script.audiooffsetmanager') - self._settings = self.addon.getSettings() - - def reload_if_needed(self): - """Public method to reload settings when explicitly needed.""" - self._settings = self.addon.getSettings() - - def _safe_setting_operation(self, operation, setting_id, default_value, value_type): - """Safely execute a settings operation with proper error handling. - - Args: - operation: The settings method to call (getBool, getInt, etc) - setting_id: The ID of the setting to access - default_value: Value to return if operation fails - value_type: String description of the value type for logging - """ - try: - return operation(setting_id) - except: - xbmc.log(f"AOM_SettingsManager: Error getting {value_type} setting " - f"'{setting_id}'. Using default: {default_value}", - xbmc.LOGWARNING) - return default_value - - def get_setting_boolean(self, setting_id): - """Retrieve the boolean setting using Settings.getBool().""" - return self._safe_setting_operation( - self._settings.getBool, - setting_id, - False, - "boolean" - ) - - def get_setting_integer(self, setting_id): - """Retrieve the integer setting using Settings.getInt().""" - return self._safe_setting_operation( - self._settings.getInt, - setting_id, - 0, - "integer" - ) - - def get_setting_string(self, setting_id): - """Retrieve the string setting using Settings.getString().""" - return self._safe_setting_operation( - self._settings.getString, - setting_id, - "", - "string" - ) - - def _safe_setting_store(self, operation, setting_id, value, value_type): - """Safely store a setting value with proper error handling. - - Args: - operation: The settings method to call (setBool, setInt, etc) - setting_id: The ID of the setting to store - value: The value to store - value_type: String description of the value type for logging - """ - try: - xbmc.log(f"AOM_SettingsManager: Storing {value_type} setting {setting_id}: " - f"{value}", xbmc.LOGDEBUG) - operation(setting_id, value) - return True - except: - xbmc.log(f"AOM_SettingsManager: Error storing {value_type} setting " - f"'{setting_id}'.", xbmc.LOGWARNING) - return False - - def store_setting_boolean(self, setting_id, value): - """Store a boolean setting.""" - return self._safe_setting_store( - self._settings.setBool, - setting_id, - value, - "boolean" - ) - - def store_setting_integer(self, setting_id, value): - """Store an integer setting.""" - return self._safe_setting_store( - self._settings.setInt, - setting_id, - value, - "integer" - ) - - def store_setting_string(self, setting_id, value): - """Store a string setting.""" - return self._safe_setting_store( - self._settings.setString, - setting_id, - value, - "string" - ) +"""Settings manager module to provide methods for other modules to interface with +the addon settings. +""" + +import xbmc +import xbmcaddon + + +class SettingsManager: + _instance = None + _settings = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(SettingsManager, cls).__new__(cls) + cls._instance._initialize() + return cls._instance + + def _initialize(self): + """Initialize the settings manager with addon settings.""" + self.addon = xbmcaddon.Addon('script.audiooffsetmanager') + self._settings = self.addon.getSettings() + + def reload_if_needed(self): + """Public method to reload settings when explicitly needed.""" + self._settings = self.addon.getSettings() + + def _safe_setting_operation(self, operation, setting_id, default_value, value_type): + """Safely execute a settings operation with proper error handling. + + Args: + operation: The settings method to call (getBool, getInt, etc) + setting_id: The ID of the setting to access + default_value: Value to return if operation fails + value_type: String description of the value type for logging + """ + try: + return operation(setting_id) + except: + xbmc.log(f"AOM_SettingsManager: Error getting {value_type} setting " + f"'{setting_id}'. Using default: {default_value}", + xbmc.LOGWARNING) + return default_value + + def get_setting_boolean(self, setting_id): + """Retrieve the boolean setting using Settings.getBool().""" + return self._safe_setting_operation( + self._settings.getBool, + setting_id, + False, + "boolean" + ) + + def get_setting_integer(self, setting_id): + """Retrieve the integer setting using Settings.getInt().""" + return self._safe_setting_operation( + self._settings.getInt, + setting_id, + 0, + "integer" + ) + + def get_setting_string(self, setting_id): + """Retrieve the string setting using Settings.getString().""" + return self._safe_setting_operation( + self._settings.getString, + setting_id, + "", + "string" + ) + + def _safe_setting_store(self, operation, setting_id, value, value_type): + """Safely store a setting value with proper error handling. + + Args: + operation: The settings method to call (setBool, setInt, etc) + setting_id: The ID of the setting to store + value: The value to store + value_type: String description of the value type for logging + """ + try: + xbmc.log(f"AOM_SettingsManager: Storing {value_type} setting {setting_id}: " + f"{value}", xbmc.LOGDEBUG) + operation(setting_id, value) + return True + except: + xbmc.log(f"AOM_SettingsManager: Error storing {value_type} setting " + f"'{setting_id}'.", xbmc.LOGWARNING) + return False + + def store_setting_boolean(self, setting_id, value): + """Store a boolean setting.""" + return self._safe_setting_store( + self._settings.setBool, + setting_id, + value, + "boolean" + ) + + def store_setting_integer(self, setting_id, value): + """Store an integer setting.""" + return self._safe_setting_store( + self._settings.setInt, + setting_id, + value, + "integer" + ) + + def store_setting_string(self, setting_id, value): + """Store a string setting.""" + return self._safe_setting_store( + self._settings.setString, + setting_id, + value, + "string" + ) diff --git a/script.audiooffsetmanager/resources/lib/stream_info.py b/script.audiooffsetmanager/resources/lib/stream_info.py index 6d2b4e9958..2dd55cbccf 100644 --- a/script.audiooffsetmanager/resources/lib/stream_info.py +++ b/script.audiooffsetmanager/resources/lib/stream_info.py @@ -12,7 +12,7 @@ def __init__(self): self.info = {} self.settings_manager = SettingsManager() self.new_install = self.settings_manager.get_setting_boolean('new_install') - self.valid_audio_formats = ['truehd', 'eac3', 'ac3', 'dtsx', 'dtshd_ma', 'dca', 'pcm'] + self.valid_audio_formats = ['truehd', 'eac3', 'ac3', 'dtshd_ma', 'dtshd_hra', 'dca', 'pcm'] self.valid_hdr_types = ['dolbyvision', 'hdr10', 'hdr10plus', 'hlg', 'sdr'] self.valid_fps_types = [23, 24, 25, 29, 30, 50, 59, 60] @@ -184,12 +184,16 @@ def get_audio_info(self, player_id): audio_channels = audio_stream.get("channels", "unknown") if audio_format != 'none': - # Advanced logic for DTS-HD MA detection - if audio_format == 'dtshd_ma' and isinstance(audio_channels, int) and audio_channels > 6: - audio_format = 'dtsx' - # New check for PCM - elif audio_format not in self.valid_audio_formats and audio_format != 'unknown': - audio_format = 'pcm' + # Check if the reported format contains any of our valid formats + reported_format = audio_format.lower() + for valid_format in self.valid_audio_formats: + if valid_format in reported_format: + audio_format = valid_format + break + else: + # If no valid format is found, assume PCM + if audio_format != 'unknown': + audio_format = 'pcm' return audio_format, audio_channels diff --git a/script.audiooffsetmanager/resources/screenshot-1.jpg b/script.audiooffsetmanager/resources/screenshot-1.jpg index 47bcc72423..996f31b243 100644 Binary files a/script.audiooffsetmanager/resources/screenshot-1.jpg and b/script.audiooffsetmanager/resources/screenshot-1.jpg differ diff --git a/script.audiooffsetmanager/resources/screenshot-2.jpg b/script.audiooffsetmanager/resources/screenshot-2.jpg index b1c961b44a..148e5fb06f 100644 Binary files a/script.audiooffsetmanager/resources/screenshot-2.jpg and b/script.audiooffsetmanager/resources/screenshot-2.jpg differ diff --git a/script.audiooffsetmanager/resources/settings.xml b/script.audiooffsetmanager/resources/settings.xml index 998feb4ac6..094f62e35f 100644 --- a/script.audiooffsetmanager/resources/settings.xml +++ b/script.audiooffsetmanager/resources/settings.xml @@ -68,7 +68,7 @@ - + @@ -134,8 +134,8 @@ - - + + @@ -156,8 +156,8 @@ - - + + @@ -223,7 +223,7 @@ - + @@ -292,8 +292,8 @@ - - + + @@ -315,8 +315,8 @@ - - + + @@ -385,7 +385,7 @@ - + @@ -454,8 +454,8 @@ - - + + @@ -477,8 +477,8 @@ - - + + @@ -547,7 +547,7 @@ - + @@ -616,8 +616,8 @@ - - + + @@ -638,9 +638,9 @@ false - - - + + + @@ -709,7 +709,7 @@ - + @@ -778,8 +778,8 @@ - - + + @@ -800,9 +800,9 @@ false - - - + + + @@ -871,7 +871,7 @@ - + @@ -940,8 +940,8 @@ - - + + @@ -962,9 +962,9 @@ false - - - + + + @@ -1033,7 +1033,7 @@ - + @@ -1102,8 +1102,8 @@ - - + + @@ -1124,9 +1124,9 @@ false - - - + + + @@ -1195,7 +1195,7 @@ - + @@ -1264,8 +1264,8 @@ - - + + @@ -1286,9 +1286,9 @@ false - - - + + + @@ -1357,7 +1357,7 @@ - + @@ -1426,8 +1426,8 @@ - - + + @@ -1448,9 +1448,9 @@ false - - - + + + @@ -1566,7 +1566,7 @@ - + @@ -1632,8 +1632,8 @@ - - + + @@ -1653,9 +1653,9 @@ false - - - + + + @@ -1721,7 +1721,7 @@ - + @@ -1790,8 +1790,8 @@ - - + + @@ -1812,9 +1812,9 @@ false - - - + + + @@ -1883,7 +1883,7 @@ - + @@ -1952,8 +1952,8 @@ - - + + @@ -1975,8 +1975,8 @@ - - + + @@ -2045,7 +2045,7 @@ - + @@ -2114,8 +2114,8 @@ - - + + @@ -2137,8 +2137,8 @@ - - + + @@ -2207,7 +2207,7 @@ - + @@ -2276,8 +2276,8 @@ - - + + @@ -2299,8 +2299,8 @@ - - + + @@ -2369,7 +2369,7 @@ - + @@ -2438,8 +2438,8 @@ - - + + @@ -2461,8 +2461,8 @@ - - + + @@ -2531,7 +2531,7 @@ - + @@ -2600,8 +2600,8 @@ - - + + @@ -2623,8 +2623,8 @@ - - + + @@ -2693,7 +2693,7 @@ - + @@ -2762,8 +2762,8 @@ - - + + @@ -2785,8 +2785,8 @@ - - + + @@ -2855,7 +2855,7 @@ - + @@ -2924,8 +2924,8 @@ - - + + @@ -2947,8 +2947,8 @@ - - + + @@ -3065,7 +3065,7 @@ - + @@ -3131,8 +3131,8 @@ - - + + @@ -3153,8 +3153,8 @@ - - + + @@ -3220,7 +3220,7 @@ - + @@ -3289,8 +3289,8 @@ - - + + @@ -3312,8 +3312,8 @@ - - + + @@ -3382,7 +3382,7 @@ - + @@ -3451,8 +3451,8 @@ - - + + @@ -3474,8 +3474,8 @@ - - + + @@ -3544,7 +3544,7 @@ - + @@ -3613,8 +3613,8 @@ - - + + @@ -3636,8 +3636,8 @@ - - + + @@ -3706,7 +3706,7 @@ - + @@ -3775,8 +3775,8 @@ - - + + @@ -3798,8 +3798,8 @@ - - + + @@ -3868,7 +3868,7 @@ - + @@ -3937,8 +3937,8 @@ - - + + @@ -3960,8 +3960,8 @@ - - + + @@ -4030,7 +4030,7 @@ - + @@ -4099,8 +4099,8 @@ - - + + @@ -4122,8 +4122,8 @@ - - + + @@ -4192,7 +4192,7 @@ - + @@ -4261,8 +4261,8 @@ - - + + @@ -4284,8 +4284,8 @@ - - + + @@ -4354,7 +4354,7 @@ - + @@ -4423,8 +4423,8 @@ - - + + @@ -4446,8 +4446,8 @@ - - + + @@ -4563,7 +4563,7 @@ - + @@ -4629,8 +4629,8 @@ - - + + @@ -4651,8 +4651,8 @@ - - + + @@ -4718,7 +4718,7 @@ - + @@ -4787,8 +4787,8 @@ - - + + @@ -4810,8 +4810,8 @@ - - + + @@ -4880,7 +4880,7 @@ - + @@ -4949,8 +4949,8 @@ - - + + @@ -4972,8 +4972,8 @@ - - + + @@ -5042,7 +5042,7 @@ - + @@ -5111,8 +5111,8 @@ - - + + @@ -5134,8 +5134,8 @@ - - + + @@ -5204,7 +5204,7 @@ - + @@ -5273,8 +5273,8 @@ - - + + @@ -5296,8 +5296,8 @@ - - + + @@ -5366,7 +5366,7 @@ - + @@ -5435,8 +5435,8 @@ - - + + @@ -5458,8 +5458,8 @@ - - + + @@ -5528,7 +5528,7 @@ - + @@ -5597,8 +5597,8 @@ - - + + @@ -5620,8 +5620,8 @@ - - + + @@ -5690,7 +5690,7 @@ - + @@ -5759,8 +5759,8 @@ - - + + @@ -5782,8 +5782,8 @@ - - + + @@ -5852,7 +5852,7 @@ - + @@ -5921,8 +5921,8 @@ - - + + @@ -5944,8 +5944,8 @@ - - + + @@ -6061,7 +6061,7 @@ - + @@ -6127,8 +6127,8 @@ - - + + @@ -6149,8 +6149,8 @@ - - + + @@ -6216,7 +6216,7 @@ - + @@ -6285,8 +6285,8 @@ - - + + @@ -6308,8 +6308,8 @@ - - + + @@ -6378,7 +6378,7 @@ - + @@ -6447,8 +6447,8 @@ - - + + @@ -6470,8 +6470,8 @@ - - + + @@ -6540,7 +6540,7 @@ - + @@ -6609,8 +6609,8 @@ - - + + @@ -6632,8 +6632,8 @@ - - + + @@ -6702,7 +6702,7 @@ - + @@ -6771,8 +6771,8 @@ - - + + @@ -6794,8 +6794,8 @@ - - + + @@ -6864,7 +6864,7 @@ - + @@ -6933,8 +6933,8 @@ - - + + @@ -6956,8 +6956,8 @@ - - + + @@ -7026,7 +7026,7 @@ - + @@ -7095,8 +7095,8 @@ - - + + @@ -7118,8 +7118,8 @@ - - + + @@ -7188,7 +7188,7 @@ - + @@ -7257,8 +7257,8 @@ - - + + @@ -7280,8 +7280,8 @@ - - + + @@ -7350,7 +7350,7 @@ - + @@ -7419,8 +7419,8 @@ - - + + @@ -7442,8 +7442,8 @@ - - + +