From d984f50489a9a22ad8ca4c17b47febcceb488e63 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Fri, 3 Nov 2023 14:39:00 +0100 Subject: [PATCH] wip --- .../lib/services/nfsession/msl/converter.py | 108 +++++++++++++----- .../services/nfsession/msl/events_handler.py | 2 +- .../lib/services/nfsession/msl/msl_handler.py | 10 +- .../lib/services/nfsession/msl/msl_utils.py | 2 +- .../services/playback/action_controller.py | 47 ++++++++ .../lib/services/playback/am_playback.py | 4 +- .../services/playback/am_section_skipping.py | 12 +- .../services/playback/am_stream_continuity.py | 31 +++-- .../services/playback/am_upnext_notifier.py | 3 + .../lib/services/playback/am_video_events.py | 20 +++- 10 files changed, 184 insertions(+), 55 deletions(-) diff --git a/resources/lib/services/nfsession/msl/converter.py b/resources/lib/services/nfsession/msl/converter.py index 3c2f12256..907319cac 100644 --- a/resources/lib/services/nfsession/msl/converter.py +++ b/resources/lib/services/nfsession/msl/converter.py @@ -21,20 +21,66 @@ def convert_to_dash(manifest): """Convert a Netflix style manifest to MPEG-DASH manifest""" # If a CDN server has stability problems it may cause errors with streaming, # we allow users to select a different CDN server - # (should be managed by ISA but is currently is not implemented) + # (should be managed automatically by add more MPD "BaseURL" tags, but is currently is not implemented in ISA) cdn_index = int(G.ADDON.getSettingString('cdn_server')[-1]) - 1 + mpd_tag = _create_mpd_tag() + + # Netflix ADS appear to have a complex customization with the browser/player this leads us to several headaches + # to be able to implement it in the add-on. + # Things to solve to have a decent ADS playback implementation: + # - Their player, once an ad is displayed, is removed from the video timeline in real time, there is no way to do + # a similar thing with this platform. But could be not a big problem, but we need somewhat find a solution + # to know when same ads is played multiple times to avoid send multiple MSL events (see next point) + # - Every time an ADS is played the website player send a MSL event like adStart/adProgress/... in similar way + # as done to send playback progress updates, his data should be related to "adverts/adBreaks" json path + # from MSL manifest data, i think this is used by netflix to know when an ad is displayed for their business. + # Here its difficult know when a specific ads is played and then make a callback to send the MSL event, due to: + # Problem 1: Player.GetProperties kodi api used in action_controller.py can provide wrong data. + # Problem 2: we should not send multiple times these events because with kodi same ads may be played more times. + # - Manifest DASH conversion problem: Im not sure how to split the main stream in the manifest in to multiple + # periods by injecting the ads in the middle of stream, because usually DASH SegmentBase needs to know the + # segments ranges (e.g. init) that we dont have(?). For now as workaround all ads (periods) are before the movie. + # - If above problem are somewhat solved, there is to find a solution for action_controller.py and his features + # in order to know when an ads is currently played, this to avoid problems such as language track selection. + # - When ADS is played you should prevent the user from skipping ads and also prevent them from forwarding the video + # now this should be managed by InputStream Adaptive addon, then changes to ISA will be required to fix this. + + ads_manifest_list = [] + if 'auxiliaryManifests' in manifest and manifest['auxiliaryManifests']: + # Find auxiliary ADS manifests + ads_manifest_list = [m for m in manifest['auxiliaryManifests'] if 'isAd' in m and m['isAd']] + + total_duration_secs = 0 + for ads_man in ads_manifest_list: + total_duration_secs += _add_period(mpd_tag, ads_man, cdn_index, total_duration_secs, False) + + total_duration_secs += _add_period(mpd_tag, manifest, cdn_index, total_duration_secs, True) + + mpd_tag.attrib['mediaPresentationDuration'] = _convert_secs_to_time(total_duration_secs) + + xml = ET.tostring(mpd_tag, encoding='utf-8', method='xml') + if LOG.is_enabled: + common.save_file_def('manifest.mpd', xml) + return xml.decode('utf-8').replace('\n', '').replace('\r', '').encode('utf-8') - seconds = manifest['duration'] / 1000 - duration = "PT" + str(int(seconds)) + ".00S" - root = _mpd_manifest_root(duration) - period = ET.SubElement(root, 'Period', start='PT0S', duration=duration) +def _add_period(mpd_tag, manifest, cdn_index, start_pts, add_pts_to_track_name): + seconds = manifest['duration'] / 1000 + movie_id = str(manifest['movieId']) + is_ads_stream = 'isAd' in manifest and manifest['isAd'] + if is_ads_stream: + movie_id += '_ads' + period_tag = ET.SubElement(mpd_tag, 'Period', id=movie_id, start=_convert_secs_to_time(start_pts), + duration=_convert_secs_to_time(seconds)) has_video_drm_streams = manifest['video_tracks'][0].get('hasDrmStreams', False) video_protection_info = _get_protection_info(manifest['video_tracks'][0]) if has_video_drm_streams else None - for video_track in manifest['video_tracks']: - _convert_video_track(video_track, period, video_protection_info, has_video_drm_streams, cdn_index) + if not add_pts_to_track_name: # workaround for kodi bug, see action_controller.py + start_pts = 0 + for index, video_track in enumerate(manifest['video_tracks']): + _convert_video_track(index, video_track, period_tag, video_protection_info, has_video_drm_streams, cdn_index, + movie_id, start_pts) common.apply_lang_code_changes(manifest['audio_tracks']) common.apply_lang_code_changes(manifest['timedtexttracks']) @@ -42,28 +88,28 @@ def convert_to_dash(manifest): has_audio_drm_streams = manifest['audio_tracks'][0].get('hasDrmStreams', False) id_default_audio_tracks = _get_id_default_audio_tracks(manifest) - for audio_track in manifest['audio_tracks']: + for index, audio_track in enumerate(manifest['audio_tracks']): is_default = audio_track['id'] == id_default_audio_tracks - _convert_audio_track(audio_track, period, is_default, has_audio_drm_streams, cdn_index) + _convert_audio_track(index, audio_track, period_tag, is_default, has_audio_drm_streams, cdn_index) - for text_track in manifest['timedtexttracks']: + for index, text_track in enumerate(manifest['timedtexttracks']): if text_track['isNoneTrack']: continue is_default = _is_default_subtitle(manifest, text_track) - _convert_text_track(text_track, period, is_default, cdn_index) + _convert_text_track(index, text_track, period_tag, is_default, cdn_index) - xml = ET.tostring(root, encoding='utf-8', method='xml') - if LOG.is_enabled: - common.save_file_def('manifest.mpd', xml) - return xml.decode('utf-8').replace('\n', '').replace('\r', '').encode('utf-8') + return seconds + + +def _convert_secs_to_time(secs): + return "PT" + str(int(secs)) + ".00S" -def _mpd_manifest_root(duration): - root = ET.Element('MPD') - root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011' - root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013' - root.attrib['mediaPresentationDuration'] = duration - return root +def _create_mpd_tag(): + mpd_tag = ET.Element('MPD') + mpd_tag.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011' + mpd_tag.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013' + return mpd_tag def _add_base_url(representation, base_url): @@ -133,10 +179,11 @@ def _add_protection_info(video_track, adaptation_set, pssh, keyid): ET.SubElement(protection, 'cenc:pssh').text = pssh -def _convert_video_track(video_track, period, protection, has_drm_streams, cdn_index): +def _convert_video_track(index, video_track, period, protection, has_drm_streams, cdn_index, movie_id, pts_offset): adaptation_set = ET.SubElement( period, # Parent 'AdaptationSet', # Tag + id=str(index), mimeType='video/mp4', contentType='video') if protection: @@ -151,12 +198,17 @@ def _convert_video_track(video_track, period, protection, has_drm_streams, cdn_i if int(downloadable['res_h']) > limit_res: continue _convert_video_downloadable(downloadable, adaptation_set, cdn_index) + # Set the name to the AdaptationSet tag + # this will become the name of the video stream, that can be read in the Kodi GUI on the video stream track list + # and can be read also by using jsonrpc Player.GetProperties "videostreams" used by action_controller.py + name = f"(Id {movie_id})(pts offset {pts_offset})" # Calculate the crop factor, will be used on am_playback.py to set zoom viewmode try: factor = video_track['maxHeight'] / video_track['maxCroppedHeight'] - adaptation_set.set('name', f'(Crop {factor:0.2f})') + name += f'(Crop {factor:0.2f})' except Exception as exc: # pylint: disable=broad-except LOG.error('Cannot calculate crop factor: {}', exc) + adaptation_set.set('name', name) def _limit_video_resolution(video_tracks, has_drm_streams): @@ -210,12 +262,12 @@ def _determine_video_codec(content_profile): if content_profile.startswith('vp9'): return f'vp9.{content_profile[11:12]}' if 'av1' in content_profile: - return 'av1' + return 'av01' return 'h264' # pylint: disable=unused-argument -def _convert_audio_track(audio_track, period, default, has_drm_streams, cdn_index): +def _convert_audio_track(index, audio_track, period, default, has_drm_streams, cdn_index): channels_count = {'1.0': '1', '2.0': '2', '5.1': '6', '7.1': '8'} impaired = 'true' if audio_track['trackType'] == 'ASSISTIVE' else 'false' original = 'true' if audio_track['isNative'] else 'false' @@ -224,6 +276,7 @@ def _convert_audio_track(audio_track, period, default, has_drm_streams, cdn_inde adaptation_set = ET.SubElement( period, # Parent 'AdaptationSet', # Tag + id=str(index), lang=audio_track['language'], contentType='audio', mimeType='audio/mp4', @@ -242,7 +295,7 @@ def _convert_audio_track(audio_track, period, default, has_drm_streams, cdn_inde def _convert_audio_downloadable(downloadable, adaptation_set, channels_count, cdn_index): - codec_type = 'aac' + codec_type = 'mp4a.40.5' # he-aac if 'ddplus-' in downloadable['content_profile'] or 'dd-' in downloadable['content_profile']: codec_type = 'ec-3' representation = ET.SubElement( @@ -261,7 +314,7 @@ def _convert_audio_downloadable(downloadable, adaptation_set, channels_count, cd _add_segment_base(representation, downloadable) -def _convert_text_track(text_track, period, default, cdn_index): +def _convert_text_track(index, text_track, period, default, cdn_index): # Only one subtitle representation per adaptationset downloadable = text_track.get('ttDownloadables') if not text_track: @@ -274,6 +327,7 @@ def _convert_text_track(text_track, period, default, cdn_index): adaptation_set = ET.SubElement( period, # Parent 'AdaptationSet', # Tag + id=str(index), lang=text_track['language'], codecs=('stpp', 'wvtt')[is_ios8], contentType='text', diff --git a/resources/lib/services/nfsession/msl/events_handler.py b/resources/lib/services/nfsession/msl/events_handler.py index c5db01b95..adbd6b3c8 100644 --- a/resources/lib/services/nfsession/msl/events_handler.py +++ b/resources/lib/services/nfsession/msl/events_handler.py @@ -146,7 +146,7 @@ def _build_event_params(self, event_type, event_data, player_state, manifest, lo # else: # list_id = G.LOCAL_DB.get_value('last_menu_id', 'unknown') - position = player_state['elapsed_seconds'] + position = player_state['current_pts'] if position != 1: position *= 1000 diff --git a/resources/lib/services/nfsession/msl/msl_handler.py b/resources/lib/services/nfsession/msl/msl_handler.py index 1f7ff0a9c..945ad7626 100644 --- a/resources/lib/services/nfsession/msl/msl_handler.py +++ b/resources/lib/services/nfsession/msl/msl_handler.py @@ -123,10 +123,10 @@ def get_manifest(self, viewable_id, challenge, sid): 'This problem could be solved in the future, but at the moment there is no solution.') raise ErrorMsgNoReport(err_msg) from exc raise - if manifest.get('adverts', {}).get('adBreaks', []): - # Todo: manifest converter should handle ads streams with additional DASH periods - raise ErrorMsgNoReport('This add-on dont support playback videos with ads. ' - 'Please use an account plan without ads.') + if G.KODI_VERSION < 20 and manifest.get('adverts', {}).get('adBreaks', []): + # InputStream Adaptive version on Kodi 19 is too old and dont handle correctly these manifests + raise ErrorMsgNoReport('On Kodi 19 the Netflix ADS plans are not supported. \n' + 'You must use Kodi 20 or higher versions.') return self._tranform_to_dash(manifest) @measure_exec_time_decorator(is_immediate=True) @@ -285,7 +285,7 @@ def _build_manifest_v2(self, **kwargs): 'requestSegmentVmaf': False, 'supportsPartialHydration': False, 'contentPlaygraph': ['start'], - 'supportsAdBreakHydration': False, + 'supportsAdBreakHydration': True, 'liveMetadataFormat': 'INDEXED_SEGMENT_TEMPLATE', 'useBetterTextUrls': True, 'profileGroups': [{ diff --git a/resources/lib/services/nfsession/msl/msl_utils.py b/resources/lib/services/nfsession/msl/msl_utils.py index 8e9eda488..123d081a9 100644 --- a/resources/lib/services/nfsession/msl/msl_utils.py +++ b/resources/lib/services/nfsession/msl/msl_utils.py @@ -84,7 +84,7 @@ def is_media_changed(previous_player_state, player_state): def update_play_times_duration(play_times, player_state): """Update the playTimes duration values""" - duration = player_state['elapsed_seconds'] * 1000 + duration = player_state['current_pts'] * 1000 play_times['total'] = duration play_times['audio'][0]['duration'] = duration play_times['video'][0]['duration'] = duration diff --git a/resources/lib/services/playback/action_controller.py b/resources/lib/services/playback/action_controller.py index 864c7252a..c1b9fa4bc 100644 --- a/resources/lib/services/playback/action_controller.py +++ b/resources/lib/services/playback/action_controller.py @@ -8,6 +8,7 @@ See LICENSES/MIT.md for more information. """ import json +import re import threading import time from typing import TYPE_CHECKING @@ -84,6 +85,7 @@ def onNotification(self, sender, method, data): # pylint: disable=unused-argume """ Callback for Kodi notifications that handles and dispatches playback events """ + LOG.warn('ActionController: onNotification {} -- {}', method, data) # WARNING: Do not get playerid from 'data', # Because when Up Next add-on play a video while we are inside Netflix add-on and # not externally like Kodi library, the playerid become -1 this id does not exist @@ -215,6 +217,20 @@ def _notify_all(self, notification, data=None): _notify_managers(manager, notification, data) def _get_player_state(self, player_id=None, time_override=None): + # !! WARNING KODI BUG ON: Player.GetProperties !! + # todo: TO TAKE IN ACCOUNT FOR FUTURE ADS IMPROVEMENTS <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + # When InputStream Adaptive add-on send the DEMUX_SPECIALID_STREAMCHANGE used to switch stream quality + # kodi core influence immediately Player.GetProperties without following what is actually played on screen + # the worst case scenario is in the case of ADS chapters, that will result we have FULL FALSE PLAYER INFO HERE! + # because while an ADS chapter is actually in playing and at same time the DEMUX_SPECIALID_STREAMCHANGE is sent + # to the kodi buffer (because after ADS the movie have to start) Player.GetProperties starts to provide + # info of the movie instead of the actually ADS in playing, this will break all add-on features around here! + + # So in short, Player.GetProperties dont provide data in realtime and depends on kodi buffer instead of + # what shown on screen. A partial solution is mapping all manifest periods streams timing here to know at least + # where are the ADS in the timeline by taking in account / checking the elapsed time only, + # and so ignoring all kodi API info try: player_state = common.json_rpc('Player.GetProperties', { 'playerid': self.active_player_id if player_id is None else player_id, @@ -258,6 +274,37 @@ def _get_player_state(self, player_id=None, time_override=None): # use saved player state player_state = self._last_player_state + # Get additional video track info added in the track name + video_stream = player_state['videostreams'][0] + # Try to find the crop info from the track name + result = re.match(r'\(Crop (\d+\.\d+)\)', video_stream['name']) + player_state['nf_video_crop_factor'] = float(result.group(1)) if result else None + # Try to find the video id from the track name (may change if ADS video parts are played) + # This value is taken from DASH manifest, see AdaptationSet tag in the converter.py + result = re.match(r'\(Id (\d+)(_[a-z]+)?\)', video_stream['name']) + player_state['nf_stream_videoid'] = result.group(1) if result else None + # Try to find the PTS offset from the track name + # The pts offset value is used with the ADS plan only, it provides the offset where the "movie" start + # since all the ADS video parts are inserted before the "movie" + result = re.match(r'\(pts offset (\d+)\)', video_stream['name']) + pts_offset = 0 + if result: + pts_offset = int(result.group(1)) + player_state['nf_is_ads_stream'] = 'ads' in video_stream['name'] + # current_pts is the current player time without the duration of previous ADS video parts (if any) + # todo: workaround for kodi bug (read above) + # make sure that current elapsed time dont fall in to ADS stream chapter + # if so fix data because Player.GetProperties provided the info of currently not played stream in advance + if not player_state['nf_is_ads_stream'] and player_state['elapsed_seconds'] <= pts_offset: + # Fall here when DEMUX_SPECIALID_STREAMCHANGE has been sent by ISAdaptive and Kodi core wrongly send + # info of next stream chapter to be played instead of the currently in playing + player_state['nf_is_ads_stream'] = True + player_state['current_pts'] = player_state['elapsed_seconds'] + else: + # We need to remove the ADS duration from the current PTS because website API dont take in account of + # each ADS time duration, ADS are handled separately as if they were not part of the video stream + player_state['current_pts'] = player_state['elapsed_seconds'] - pts_offset + player_state['nf_pts_offset'] = pts_offset return player_state diff --git a/resources/lib/services/playback/am_playback.py b/resources/lib/services/playback/am_playback.py index c06424e32..0f61afc6b 100644 --- a/resources/lib/services/playback/am_playback.py +++ b/resources/lib/services/playback/am_playback.py @@ -85,6 +85,8 @@ def on_playback_resume(self, player_state): self.is_player_in_pause = False def on_playback_stopped(self, player_state): + if player_state['nf_is_ads_stream']: + return # It could happen that Kodi does not assign as watched a video, # this because the credits can take too much time, then the point where playback is stopped # falls in the part that kodi recognizes as unwatched (playcountminimumpercent 90% + no-mans land 2%) @@ -92,7 +94,7 @@ def on_playback_stopped(self, player_state): # In these cases we try change/fix manually the watched status of the video by using netflix offset data if int(player_state['percentage']) > 92: return - if not self.watched_threshold or not player_state['elapsed_seconds'] > self.watched_threshold: + if not self.watched_threshold or not player_state['current_pts'] > self.watched_threshold: return if G.ADDON.getSettingBool('sync_watched_status') and not self.is_played_from_strm: # This have not to be applied with our custom watched status of Netflix sync, within the addon diff --git a/resources/lib/services/playback/am_section_skipping.py b/resources/lib/services/playback/am_section_skipping.py index bc649d39f..6c47b9897 100644 --- a/resources/lib/services/playback/am_section_skipping.py +++ b/resources/lib/services/playback/am_section_skipping.py @@ -29,6 +29,7 @@ def __init__(self): self.markers = {} self.auto_skip = False self.pause_on_skip = False + self.pts_offset = 0 def __str__(self): return f'enabled={self.enabled}, markers={self.markers}, auto_skip={self.auto_skip}, pause_on_skip={self.pause_on_skip}' @@ -39,8 +40,11 @@ def initialize(self, data): self.pause_on_skip = G.ADDON.getSettingBool('pause_on_skip') def on_tick(self, player_state): + if player_state['nf_is_ads_stream']: + return + self.pts_offset = player_state['nf_pts_offset'] for section in SKIPPABLE_SECTIONS: - self._check_section(section, player_state['elapsed_seconds']) + self._check_section(section, player_state['current_pts']) def _check_section(self, section, elapsed): if self.markers.get(section) and self.markers[section]['start'] <= elapsed <= self.markers[section]['end']: @@ -62,18 +66,18 @@ def _auto_skip(self, section): if self.pause_on_skip: player.pause() xbmc.sleep(1000) # give kodi the chance to execute - player.seekTime(self.markers[section]['end']) + player.seekTime(self.markers[section]['end'] + self.pts_offset) xbmc.sleep(1000) # give kodi the chance to execute player.pause() # unpause playback at seek position else: - player.seekTime(self.markers[section]['end']) + player.seekTime(self.markers[section]['end'] + self.pts_offset) def _ask_to_skip(self, section): LOG.debug('Asking to skip {}', section) dialog_duration = (self.markers[section]['end'] - self.markers[section]['start']) ui.show_skip_dialog(dialog_duration, - seek_time=self.markers[section]['end'], + seek_time=self.markers[section]['end'] + self.pts_offset, label=common.get_local_string(SKIPPABLE_SECTIONS[section])) def on_playback_stopped(self, player_state): diff --git a/resources/lib/services/playback/am_stream_continuity.py b/resources/lib/services/playback/am_stream_continuity.py index 969013e57..e7cb1356d 100644 --- a/resources/lib/services/playback/am_stream_continuity.py +++ b/resources/lib/services/playback/am_stream_continuity.py @@ -61,6 +61,7 @@ def __init__(self): self.is_kodi_forced_subtitles_only = None self.is_prefer_alternative_lang = None self.ignore_av_change_event = False + self.need_delay_init = False def __str__(self): return f'enabled={self.enabled}, videoid_parent={self.videoid_parent}' @@ -70,6 +71,13 @@ def initialize(self, data): self.is_prefer_alternative_lang = G.ADDON.getSettingBool('prefer_alternative_lang') def on_playback_started(self, player_state): # pylint: disable=too-many-branches + # TODO: disabled + if player_state['nf_is_ads_stream']: + self.need_delay_init = True + else: + self._init(player_state) + + def _init(self, player_state): is_enabled = G.ADDON.getSettingBool('StreamContinuityManager_enabled') # remember audio/subtitle preferences if is_enabled: # Get user saved preferences @@ -146,6 +154,11 @@ def on_playback_started(self, player_state): # pylint: disable=too-many-branche def on_tick(self, player_state): self.player_state = player_state + if player_state['nf_is_ads_stream']: + return + if self.need_delay_init: + self._init(player_state) + self.need_delay_init = False # Check if the audio stream is changed current_stream = self.current_streams['audio'] player_stream = player_state.get(STREAMS['audio']['current']) @@ -184,6 +197,8 @@ def on_tick(self, player_state): LOG.debug('subtitleenabled has changed from {} to {}', current_stream, player_stream) def on_playback_avchange_delayed(self, player_state): + if player_state['nf_is_ads_stream']: + return if self.ignore_av_change_event: self.ignore_av_change_event = False return @@ -492,12 +507,10 @@ def _determine_fixed_zoom_factor(player_state): # Calculate the zoom factor based on the percentage of the portion of the screen which black bands can occupy, # by taking in account that each video may have a different crop value. blackbar_perc = G.ADDON.getSettingInt('blackbars_minimizer_value') - # Try to find the crop info to the track name - stream = player_state['videostreams'][0] - result = re.match(r'\(Crop (\d+\.\d+)\)', stream['name']) + crop_factor = player_state['nf_video_crop_factor'] zoom_factor = 1.0 - if result: - crop_factor = float(result.group(1)) + if crop_factor: + stream = player_state['videostreams'][0] stream_height = stream['height'] video_height = stream['height'] / crop_factor blackbar_px = stream_height - video_height @@ -513,12 +526,10 @@ def _determine_relative_zoom_factor(player_state): # NOTE: Has been chosen to calculate the factor by using video height px instead of black bands height px # to have a more short scale for the user setting blackbar_perc = G.ADDON.getSettingInt('blackbars_minimizer_value') - # Try to find the crop info to the track name - stream = player_state['videostreams'][0] - result = re.match(r'\(Crop (\d+\.\d+)\)', stream['name']) + crop_factor = player_state['nf_video_crop_factor'] zoom_factor = 1.0 - if result: - crop_factor = float(result.group(1)) + if crop_factor: + stream = player_state['videostreams'][0] stream_height = stream['height'] video_height = stream['height'] / crop_factor video_zoomed_h = video_height + (video_height / 100 * blackbar_perc) diff --git a/resources/lib/services/playback/am_upnext_notifier.py b/resources/lib/services/playback/am_upnext_notifier.py index 9eb10d5a4..fa6e02994 100644 --- a/resources/lib/services/playback/am_upnext_notifier.py +++ b/resources/lib/services/playback/am_upnext_notifier.py @@ -58,6 +58,9 @@ def initialize(self, data): LOG.warn('Up Next add-on signal skipped, the videoid for the next episode does not exist in the database') def on_playback_started(self, player_state): # pylint: disable=unused-argument + # TODO: disabled, to take in account ads duration + if player_state['nf_is_ads_stream']: + return if self.upnext_info: LOG.debug('Sending initialization signal to Up Next Add-on') import AddonSignals diff --git a/resources/lib/services/playback/am_video_events.py b/resources/lib/services/playback/am_video_events.py index 9b846c2b5..a8823d181 100644 --- a/resources/lib/services/playback/am_video_events.py +++ b/resources/lib/services/playback/am_video_events.py @@ -78,6 +78,8 @@ def on_playback_started(self, player_state): pass def on_tick(self, player_state): + if player_state['nf_is_ads_stream']: + return if self.lock_events: return if self.is_player_in_pause and (self.tick_elapsed - self.last_tick_count) >= 1800: @@ -91,9 +93,9 @@ def on_tick(self, player_state): # We do not use _on_playback_started() to send EVENT_START, because the action managers # AMStreamContinuity and AMPlayback may cause inconsistencies with the content of player_state data - # When the playback starts for the first time, for correctness should send elapsed_seconds value to 1 + # When the playback starts for the first time, for correctness should send current_pts value to 1 if self.tick_elapsed < 5 and self.event_data['resume_position'] is None: - player_state['elapsed_seconds'] = 1 + player_state['current_pts'] = 1 self._send_event(EVENT_START, self.event_data, player_state) self.is_event_start_sent = True self.tick_elapsed = 0 @@ -101,7 +103,7 @@ def on_tick(self, player_state): # Generate events to send to Netflix service every 1 minute (60secs=1m) if (self.tick_elapsed - self.last_tick_count) >= 60: self._send_event(EVENT_KEEP_ALIVE, self.event_data, player_state) - self._save_resume_time(player_state['elapsed_seconds']) + self._save_resume_time(player_state['current_pts']) self.last_tick_count = self.tick_elapsed # Allow request of loco update (for continueWatching and bookmark) only after the first minute # it seems that most of the time if sent earlier returns error @@ -109,34 +111,40 @@ def on_tick(self, player_state): self.tick_elapsed += 1 # One tick almost always represents one second def on_playback_pause(self, player_state): + if player_state['nf_is_ads_stream']: + return if not self.is_event_start_sent: return self._reset_tick_count() self.is_player_in_pause = True self._send_event(EVENT_ENGAGE, self.event_data, player_state) - self._save_resume_time(player_state['elapsed_seconds']) + self._save_resume_time(player_state['current_pts']) def on_playback_resume(self, player_state): self.is_player_in_pause = False self.lock_events = False def on_playback_seek(self, player_state): + if player_state['nf_is_ads_stream']: + return if not self.is_event_start_sent or self.lock_events: # This might happen when the action manager AMPlayback perform a video skip return self._reset_tick_count() self._send_event(EVENT_ENGAGE, self.event_data, player_state) - self._save_resume_time(player_state['elapsed_seconds']) + self._save_resume_time(player_state['current_pts']) self.allow_request_update_loco = True def on_playback_stopped(self, player_state): + if player_state['nf_is_ads_stream']: + return if not self.is_event_start_sent or self.lock_events: return self._reset_tick_count() self._send_event(EVENT_ENGAGE, self.event_data, player_state) self._send_event(EVENT_STOP, self.event_data, player_state) # Update the resume here may not always work due to race conditions with GUI directory refresh and Stop event - self._save_resume_time(player_state['elapsed_seconds']) + self._save_resume_time(player_state['current_pts']) def _save_resume_time(self, resume_time): """Save resume time value in order to update the infolabel cache"""