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.")
|