Skip to content

Commit

Permalink
Fix unwanted virtual subclip behaviour (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
mediaminister authored Sep 18, 2020
1 parent 0c9d587 commit 9b7105d
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 34 deletions.
40 changes: 39 additions & 1 deletion resources/lib/playerinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
41 changes: 24 additions & 17 deletions resources/lib/streamservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
16 changes: 0 additions & 16 deletions tests/test_streamservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 9b7105d

Please sign in to comment.