1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
# This is based on https://github.com/jellyfin/jellyfin-web/blob/master/src/components/syncPlay/timeSyncManager.js
import threading
import logging
import datetime
LOG = logging.getLogger('Jellyfin.' + __name__)
number_of_tracked_measurements = 8
polling_interval_greedy = 1
polling_interval_low_profile = 60
greedy_ping_count = 3
class Measurement:
def __init__(self, request_sent, request_received, response_sent, response_received):
self.request_sent = request_sent
self.request_received = request_received
self.response_sent = response_sent
self.response_received = response_received
def get_offset(self):
"""Time offset from server."""
return ((self.request_received - self.request_sent) + (self.response_sent - self.response_received)) / 2.0
def get_delay(self):
"""Get round-trip delay."""
return (self.response_received - self.request_sent) - (self.response_sent - self.request_received)
def get_ping(self):
"""Get ping time."""
return self.get_delay() / 2.0
class _TimeSyncThread(threading.Thread):
def __init__(self, manager):
self.manager = manager
self.halt = threading.Event()
threading.Thread.__init__(self)
def run(self):
while not self.halt.wait(self.manager.polling_interval):
try:
measurement = self.manager.client.jellyfin.utc_time()
measurement = Measurement(measurement["request_sent"], measurement["request_received"],
measurement["response_sent"], measurement["response_received"])
self.manager.update_time_offset(measurement)
if self.manager.pings > greedy_ping_count:
self.manager.polling_interval = polling_interval_low_profile
else:
self.manager.pings += 1
self.manager._notify_subscribers()
except Exception:
LOG.error("Timesync call failed.", exc_info=True)
def stop(self):
self.halt.set()
self.join()
class TimeSyncManager:
def __init__(self, client):
self.ping_stop = True
self.polling_interval = polling_interval_greedy
self.poller = None
self.pings = 0 # number of pings
self.measurement = None # current time sync
self.measurements = []
self.client = client
self.timesync_thread = None
self.subscribers = set()
def is_ready(self):
"""Gets status of time sync."""
return self.measurement is not None
def get_time_offset(self):
"""Gets time offset with server."""
return self.measurement.get_offset() if self.measurement is not None else datetime.timedelta(0)
def get_ping(self):
"""Gets ping time to server."""
return self.measurement.get_ping() if self.measurement is not None else datetime.timedelta(0)
def update_time_offset(self, measurement):
"""Updates time offset between server and client."""
self.measurements.append(measurement)
if len(self.measurements) > number_of_tracked_measurements:
self.measurements.pop(0)
self.measurement = min(self.measurements, key=lambda x: x.get_delay())
def reset_measurements(self):
"""Drops accumulated measurements."""
self.measurement = None
self.measurements = []
def start_ping(self):
"""Starts the time poller."""
if not self.timesync_thread:
self.timesync_thread = _TimeSyncThread(self)
self.timesync_thread.start()
def stop_ping(self):
"""Stops the time poller."""
if self.timesync_thread:
self.timesync_thread.stop()
self.timesync_thread = None
def force_update(self):
"""Resets poller into greedy mode."""
self.stop_ping()
self.polling_interval = polling_interval_greedy
self.pings = 0
self.start_ping()
def server_date_to_local(self, server):
"""Converts server time to local time."""
return server - self.get_time_offset()
def local_date_to_server(self, local):
"""Converts local time to server time."""
return local + self.get_time_offset()
def subscribe_time_offset(self, subscriber_callable):
"""Pass a callback function to get notified about time offset changes."""
self.subscribers.add(subscriber_callable)
def remove_subscriber(self, subscriber_callable):
"""Remove a callback function from notifications."""
self.subscribers.remove(subscriber_callable)
def _notify_subscribers(self):
for subscriber in self.subscribers:
try:
subscriber(self.get_time_offset(), self.get_ping())
except Exception:
LOG.error("Exception in subscriber callback.")
|