diff --git a/resources/lib/playerinfo.py b/resources/lib/playerinfo.py index 458bf730..3ca61b02 100644 --- a/resources/lib/playerinfo.py +++ b/resources/lib/playerinfo.py @@ -9,7 +9,7 @@ from apihelper import ApiHelper from data import CHANNELS from favorites import Favorites -from kodiutils import addon_id, get_setting_bool, has_addon, kodi_version_major, log, notify, set_property +from kodiutils import addon_id, get_setting_bool, has_addon, jsonrpc, kodi_version_major, log, log_error, notify, set_property from resumepoints import ResumePoints from utils import play_url_to_id, to_unicode, url_to_episode @@ -102,6 +102,7 @@ def onAVStarted(self): # pylint: disable=invalid-name if not self.listen: return log(3, '[PlayerInfo {id}] Event onAVStarted', id=self.thread_id) + self.virtualsubclip_seektozero() self.quit.clear() self.update_position() self.update_total() @@ -224,6 +225,43 @@ def update_position(self): except RuntimeError: pass + def virtualsubclip_seektozero(self): + """VRT NU already offers some programs (mostly current affairs programs) as video on demand while the program is still being broadcasted live. + To do so, a start timestamp is added to the livestream url so the Unified Origin streaming platform knows + it should return a time bounded manifest file that indicates the beginning of the program. + This is called a Live-to-VOD stream or virtual subclip: https://docs.unified-streaming.com/documentation/vod/player-urls.html#virtual-subclips + e.g. https://live-cf-vrt.akamaized.net/groupc/live/8edf3bdf-7db3-41c3-a318-72cb7f82de66/live.isml/.mpd?t=2020-07-20T11:07:00 + + For some unclear reason the virtual subclip defined by a single start timestamp still behaves as a ordinary livestream + and starts at the live edge of the stream. It seems this is not a Kodi or Inputstream Adaptive bug, because other players + like THEOplayer or DASH-IF's reference player treat this kind of manifest files the same way. + The only difference is that seeking to the beginning of the program is possible. So if the url contains a single start timestamp, + we can work around this problem by automatically seeking to the beginning of the program. + """ + playing_file = self.getPlayingFile() + if '?t=' in playing_file: + try: # Python 3 + from urllib.parse import parse_qs, urlsplit + except ImportError: # Python 2 + from urlparse import parse_qs, urlsplit + import re + # Detect single start timestamp + timestamp = parse_qs(urlsplit(playing_file).query).get('t')[0] + rgx = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') + is_single_start_timestamp = bool(re.match(rgx, timestamp)) + if is_single_start_timestamp: + # Check resume status + resume_info = jsonrpc(method='Player.GetItem', params=dict(playerid=1, properties=['resume'])).get('result') + if resume_info: + resume_position = resume_info.get('item').get('resume').get('position') + is_resumed = abs(resume_position - self.getTime()) < 1 + # Seek to zero if the user didn't resume the program + if not is_resumed: + log(3, '[PlayerInfo {id}] Virtual subclip: seeking to the beginning of the program', id=self.thread_id) + self.seekTime(0) + else: + log_error('Failed to start virtual subclip {playing_file} at start timestamp', playing_file=playing_file) + def update_total(self): """Update the total video time""" try: diff --git a/resources/lib/streamservice.py b/resources/lib/streamservice.py index 188f0628..fc6ef028 100644 --- a/resources/lib/streamservice.py +++ b/resources/lib/streamservice.py @@ -154,28 +154,35 @@ def _get_stream_json(self, api_data, roaming=False): @staticmethod def _fix_virtualsubclip(manifest_url, duration): - '''VRT NU already offers some programs (mostly current affairs programs) as video on demand from the moment the live broadcast has started. + """VRT NU already offers some programs (mostly current affairs programs) as video on demand from the moment the live broadcast has started. To do so, VRT NU adds start (and stop) timestamps to the livestream url to indicate the beginning (and the end) of a program. So Unified Origin streaming platform knows it should return a time bounded manifest file, this is called a Live-to-VOD stream or virtual subclip: https://docs.unified-streaming.com/documentation/vod/player-urls.html#virtual-subclips e.g. https://live-cf-vrt.akamaized.net/groupc/live/8edf3bdf-7db3-41c3-a318-72cb7f82de66/live.isml/.mpd?t=2020-07-20T11:07:00 - But Unified Origin streaming platform needs a past stop timestamp to return a virtual subclip. - - When a past stop timestamp is missing Unified Origin streaming platform treats the stream as an ordinary livestream - and doesn't return a virtual subclip. Therefore we must try to add a past stop timestamp to the manifest_url.''' - begin = manifest_url.split('?t=')[1] if '?t=' in manifest_url else None - if begin and len(begin) == 19: - from datetime import datetime, timedelta - import dateutil.parser - begin_time = dateutil.parser.parse(begin) - end_time = begin_time + duration - # We always need a past stop timestamp so if a program is not yet broadcasted completely, - # we should use the current time minus 5 seconds safety margin as a stop timestamp. - now = datetime.utcnow() - if end_time > now: - end_time = now - timedelta(seconds=5) - manifest_url += '-' + end_time.strftime('%Y-%m-%dT%H:%M:%S') + Right after a program is completely broadcasted, the stop timestamp is usually missing and should be added to the manifest_url. + """ + if '?t=' in manifest_url: + try: # Python 3 + from urllib.parse import parse_qs, urlsplit + except ImportError: # Python 2 + from urlparse import parse_qs, urlsplit + import re + + # Detect single start timestamp + begin = parse_qs(urlsplit(manifest_url).query).get('t')[0] + rgx = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') + is_single_start_timestamp = bool(re.match(rgx, begin)) + if begin and is_single_start_timestamp: + from datetime import datetime, timedelta + import dateutil.parser + begin_time = dateutil.parser.parse(begin) + # Calculate end_time with a safety margin + end_time = begin_time + duration + timedelta(seconds=10) + # Add stop timestamp if a program is broadcasted completely + now = datetime.utcnow() + if end_time < now: + manifest_url += '-' + end_time.strftime('%Y-%m-%dT%H:%M:%S') return manifest_url def get_stream(self, video, roaming=False, api_data=None): diff --git a/tests/test_streamservice.py b/tests/test_streamservice.py index 3f766c58..14347c0b 100644 --- a/tests/test_streamservice.py +++ b/tests/test_streamservice.py @@ -72,22 +72,6 @@ def test_get_ondemand_stream_from_url_gets_stream_does_not_crash(self): stream = self._streamservice.get_stream(video) self.assertTrue(stream is not None) - @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') - @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') - def test_get_journaal_stream_from_url_test_virtual_subclip_format(self): - """Test if virtual subclip stream_url has a past stop timestamp""" - video = dict(video_url='https://www.vrt.be/vrtnu/a-z/het-journaal.relevant/', - video_id=None, - publication_id=None) - stream = self._streamservice.get_stream(video) - if '?t=' in stream.stream_url: - import re - timestamps = stream.stream_url.split('?t=')[1] - rgx = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') - end_time = dateutil.parser.parse(timestamps[20:39], default=datetime.now(dateutil.tz.UTC)) - self.assertTrue(bool(re.match(rgx, timestamps))) - self.assertTrue(bool(end_time < now)) - def test_get_mpd_live_stream_from_url_does_not_crash_returns_stream_and_licensekey(self): """Test getting MPD stream from URL""" addon.settings['usedrm'] = True