pandora_t_for_time/player/player.py

449 lines
16 KiB
Python
Raw Normal View History

2023-10-30 19:26:11 +00:00
#!/usr/bin/python3
import argparse
2024-01-22 14:06:40 +00:00
import collections
import json
2023-10-30 19:26:11 +00:00
import os
import socket
import time
from threading import Thread
2023-11-08 08:18:55 +00:00
from datetime import datetime
2023-10-30 19:26:11 +00:00
import ox
2023-10-30 19:26:11 +00:00
import mpv
import logging
logger = logging.getLogger('t_for_time')
SYNC_TOLERANCE = 0.05
SYNC_GRACE_TIME = 5
SYNC_JUMP_AHEAD = 1
PORT = 9067
DEBUG = False
2024-04-01 10:08:21 +00:00
CONFIG = {
"font": "Menlo",
"font_size": 30,
"font_border": 4,
2024-04-02 10:35:01 +00:00
"sub_border_color": "0.0/0.0/0.0/0.75",
2024-04-01 10:08:21 +00:00
"sub_margin": 2 * 36 + 6,
"sub_spacing": 0,
2024-09-10 11:42:16 +00:00
"vf": None,
"sync_group": None,
2024-04-01 10:08:21 +00:00
}
2023-10-30 19:26:11 +00:00
2023-11-20 23:27:49 +00:00
def hide_gnome_overview():
import dbus
bus = dbus.SessionBus()
shell = bus.get_object('org.gnome.Shell', '/org/gnome/Shell')
props = dbus.Interface(shell, 'org.freedesktop.DBus.Properties')
props.Set('org.gnome.Shell', 'OverviewActive', False)
2023-10-30 19:26:11 +00:00
def mpv_log(loglevel, component, message):
logger.info('[{}] {}: {}'.format(loglevel, component, message))
class Main:
playlist_current_pos = -1
time_pos = -1
class Sync(Thread):
active = True
is_main = True
2024-04-04 22:24:56 +00:00
is_paused = False
2023-10-30 19:26:11 +00:00
ready = False
destination = "255.255.255.255"
2023-10-31 09:03:57 +00:00
reload_check = None
2023-11-08 08:18:55 +00:00
_pos = None
2023-11-08 10:01:02 +00:00
_tick = 0
2023-11-14 11:26:33 +00:00
need_to_sync = False
2023-10-30 19:26:11 +00:00
def __init__(self, *args, **kwargs):
self.is_main = kwargs.get('mode', 'main') == 'main'
self.start_at_hour = kwargs.get("hour", False)
2023-10-30 19:26:11 +00:00
self.sock = self.init_socket()
self.main = Main()
if self.is_main:
self.socket_enable_broadcast()
if kwargs.get("sax"):
self.sax = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
)
self.sax.loop_file = True
self.sax.play("/srv/t_for_time/render/Saxophone-5.1.mp4")
else:
self.sax = None
2024-01-22 14:06:40 +00:00
if mpv.MPV_VERSION >= (2, 2):
self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
2024-04-01 10:08:21 +00:00
sub_font_size=CONFIG["font_size"], sub_font=CONFIG["font"],
sub_border_size=CONFIG["font_border"],
2024-04-02 10:35:01 +00:00
sub_border_color=CONFIG["sub_border_color"],
2024-04-01 10:08:21 +00:00
sub_margin_y=CONFIG["sub_margin"],
sub_ass_line_spacing=CONFIG["sub_spacing"],
2024-01-22 14:06:40 +00:00
)
else:
self.mpv = mpv.MPV(
log_handler=mpv_log, input_default_bindings=True,
input_vo_keyboard=True,
2024-04-01 10:08:21 +00:00
sub_text_font_size=CONFIG["font_size"], sub_text_font=CONFIG["font"],
sub_border_size=CONFIG["font_border"],
2024-04-02 10:35:01 +00:00
sub_border_color=CONFIG["sub_border_color"],
2024-04-01 10:08:21 +00:00
sub_margin_y=CONFIG["sub_margin"],
sub_ass_line_spacing=CONFIG["sub_spacing"],
2024-01-22 14:06:40 +00:00
)
2024-04-01 10:08:21 +00:00
if CONFIG.get("vf"):
self.mpv.vf = CONFIG["vf"]
2023-10-30 19:26:11 +00:00
self.mpv.observe_property('time-pos', self.time_pos_cb)
self.mpv.fullscreen = kwargs.get('fullscreen', False)
2023-10-31 10:18:28 +00:00
self.mpv.loop_file = False
self.mpv.loop_playlist = True
2023-10-30 19:26:11 +00:00
self.mpv.register_key_binding('q', self.q_binding)
self.mpv.register_key_binding('s', self.s_binding)
self.mpv.register_key_binding('p', self.p_binding)
2024-03-23 09:00:47 +00:00
self.mpv.register_key_binding('SPACE', self.space_binding)
2023-10-30 19:26:11 +00:00
self.playlist = kwargs['playlist']
2023-10-31 09:03:57 +00:00
self.playlist_mtime = os.stat(self.playlist).st_mtime
2023-10-30 19:26:11 +00:00
self.mpv.loadlist(self.playlist)
2023-10-31 09:03:57 +00:00
logger.error("loaded paylist: %s", self.playlist)
2024-01-22 14:06:40 +00:00
logger.debug("current playlist: %s", json.dumps(self.mpv.playlist, indent=2))
2023-10-30 19:26:11 +00:00
self.deviations = collections.deque(maxlen=10)
if not self.is_main:
self.mpv.pause = False
time.sleep(0.1)
self.mpv.pause = True
self.sync_to_main()
elif self.start_at_hour:
self.mpv.pause = True
fmt = '%Y-%m-%d %H'
now = datetime.now()
offset = (now - datetime.strptime(now.strftime(fmt), fmt)).total_seconds()
if self.sax:
2024-03-22 13:51:08 +00:00
self.sax.wait_until_playing()
self.sax.seek(offset, 'absolute', 'exact')
self.sax.pause = True
position = 0
for idx, item in enumerate(self.mpv.playlist):
duration = ox.avinfo(item['filename'])['duration']
if position + duration > offset:
pos = offset - position
self.mpv.playlist_play_index(idx)
self.mpv.pause = False
self.mpv.wait_until_playing()
self.mpv.seek(pos, 'absolute', 'exact')
time.sleep(0.1)
break
else:
position += duration
if self.sax:
self.sax.pause = False
2023-10-30 19:26:11 +00:00
self.ready = True
Thread.__init__(self)
self.start()
def run(self):
while self.active:
if self.is_main:
2023-10-31 09:03:57 +00:00
time.sleep(0.5)
2023-10-30 19:26:11 +00:00
else:
2023-11-14 11:26:33 +00:00
if self.need_to_sync:
self.sync_to_main()
self.deviations = collections.deque(maxlen=10)
2023-11-14 11:26:33 +00:00
self.need_to_sync = False
2023-11-14 15:32:27 +00:00
else:
self.read_position_main()
2023-10-31 09:03:57 +00:00
self.reload_playlist()
2024-04-04 22:24:56 +00:00
if not self.is_paused and self._tick and abs(time.time() - self._tick) > 60:
2023-11-08 10:01:02 +00:00
logger.error("player is stuck")
self._tick = 0
2023-11-08 23:32:32 +00:00
self.stop()
self.mpv.stop()
2023-10-30 19:26:11 +00:00
def q_binding(self, *args):
2024-03-23 09:00:47 +00:00
if args[0] != 'd-':
return
2023-10-30 19:26:11 +00:00
self.stop()
self.mpv.stop()
2024-03-23 09:00:47 +00:00
def space_binding(self, *args):
if args[0] != 'd-':
return
if self.mpv.pause:
self.p_binding(*args)
else:
self.s_binding(*args)
def s_binding(self, *args):
2024-03-23 09:00:47 +00:00
if args[0] != 'd-':
return
2024-04-04 22:24:56 +00:00
self.is_paused = True
self.mpv.pause = True
if self.sax:
self.sax.pause = True
2024-03-23 09:00:47 +00:00
self.send_playback_state()
def p_binding(self, *args):
2024-03-23 09:00:47 +00:00
if args[0] != 'd-':
return
2024-04-04 22:24:56 +00:00
self.is_paused = False
2024-04-04 22:37:29 +00:00
self._tick = 0
self.mpv.pause = False
if self.sax:
self.sax.pause = False
2024-03-23 09:00:47 +00:00
self.send_playback_state()
2023-10-30 19:26:11 +00:00
def stop(self, *args):
self.active = False
if self.sock:
self.sock.close()
self.sock = None
def time_pos_cb(self, pos, *args, **kwargs):
2023-11-08 10:01:02 +00:00
self._tick = time.time()
2023-10-30 19:26:11 +00:00
if self.is_main:
self.send_position_local()
elif self.ready:
self.adjust_position()
2023-11-08 08:18:55 +00:00
if self._pos != self.mpv.playlist_current_pos:
self._pos = self.mpv.playlist_current_pos
2023-11-20 11:28:47 +00:00
self.deviations = collections.deque(maxlen=10)
2023-11-20 11:39:02 +00:00
self.need_to_sync = False
2023-11-08 09:23:06 +00:00
try:
track = self.mpv.playlist[self._pos]
logger.error("%s %s", datetime.now(), track["filename"])
except:
pass
2023-10-30 19:26:11 +00:00
2023-10-31 09:03:57 +00:00
def reload_playlist(self):
if not self.reload_check:
self.reload_check = time.time()
if time.time() - self.reload_check > 5:
self.reload_check = time.time()
playlist_mtime = os.stat(self.playlist).st_mtime
if self.playlist_mtime != playlist_mtime:
self.playlist_mtime = playlist_mtime
2024-01-22 14:06:40 +00:00
#self.mpv.loadlist(self.playlist)
with open(self.playlist) as fd:
items = fd.read().strip().split('\n')
base = os.path.dirname(self.playlist)
items = [os.path.join(base, item) for item in items]
current_items = self.mpv.playlist_filenames
for filename in items:
if filename not in current_items:
self.mpv.playlist_append(filename)
logger.error("add: %s", filename)
remove = []
for filename in current_items:
if filename not in items:
remove.append(filename)
for filename in remove:
for idx, item in enumerate(self.mpv.playlist):
if item["filename"] == filename:
logger.error("remove: %s %s", idx, filename)
self.mpv.playlist_remove(idx)
break
for idx, filename in enumerate(items):
current_idx = self.mpv.playlist_filenames.index(filename)
if idx != current_idx:
logger.error("move item %s %s -> %s", filename, current_idx, idx)
self.mpv.playlist_move(current_idx, idx)
2023-10-31 09:03:57 +00:00
logger.error("reloaded paylist: %s", self.playlist)
2024-01-22 14:06:40 +00:00
logger.debug("current playlist: %s", json.dumps(self.mpv.playlist, indent=2))
2023-10-30 19:26:11 +00:00
def init_socket(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", PORT))
return sock
#
# main specific
#
def socket_enable_broadcast(self):
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.sock.connect((self.destination, PORT))
def send_position_local(self):
if not self.active:
return
try:
msg = (
"%0.4f %s"
% (self.mpv.time_pos, self.mpv.playlist_current_pos)
).encode()
2024-09-10 11:42:16 +00:00
if CONFIG.get("sync_group"):
msg = (b"%s " % CONFIG["sync_group"]) + msg
2023-10-30 19:26:11 +00:00
except:
return
try:
self.sock.send(msg)
except socket.error as e:
logger.error("send failed: %s", e)
2024-03-23 09:00:47 +00:00
def send_playback_state(self):
state = 'pause' if self.mpv.pause else 'play'
msg = ("%s -1" % state).encode()
try:
self.sock.send(msg)
except socket.error as e:
logger.error("send failed: %s", e)
2023-10-30 19:26:11 +00:00
#
# follower specific
#
2024-03-23 09:00:47 +00:00
_last_ping = None
2023-10-30 19:26:11 +00:00
def read_position_main(self):
2023-11-08 09:23:06 +00:00
self.sock.settimeout(5)
2024-09-10 11:46:47 +00:00
while True:
try:
data = self.sock.recvfrom(1024)[0].decode().split(" ", 1)
except socket.timeout:
if self._last_ping != "pause":
logger.error("failed to receive data from main")
return
except OSError:
logger.error("socket closed")
return
2024-09-10 11:42:16 +00:00
if CONFIG.get("sync_group"):
2024-09-10 11:46:47 +00:00
if data[0] == str(CONFIG["sync_group"]):
2024-09-10 11:42:16 +00:00
data = data[1:]
2024-09-10 11:46:47 +00:00
break
2024-03-23 09:00:47 +00:00
else:
2024-09-10 11:46:47 +00:00
break
self._last_ping = data[0]
if data[0] == "pause":
self.is_paused = True
self.mpv.pause = True
elif data[0] == "play":
self.is_paused = False
self._tick = 0
self.mpv.pause = False
else:
self.main.time_pos = float(data[0])
self.main.playlist_current_pos = int(data[1])
2023-10-30 19:26:11 +00:00
def adjust_position(self):
if self.mpv.time_pos is not None:
try:
deviation = self.main.time_pos - self.mpv.time_pos
except:
return
2023-10-30 19:26:11 +00:00
self.deviations.append(deviation)
median_deviation = self.median(list(self.deviations))
frames = deviation / 0.04
median_frames = median_deviation / 0.04
if abs(deviation) <= 0.04 and self.mpv.speed != 1.0:
self.mpv.speed = 1.0
logger.error(
'%0.05f back to normal speed %0.05f (%d) median %0.05f (%d) -> %s' % (self.mpv.time_pos, deviation, frames, median_deviation, median_frames, self.mpv.speed)
)
2023-10-30 19:26:11 +00:00
if time.time() - self.last_sync > SYNC_GRACE_TIME and abs(median_deviation) > SYNC_TOLERANCE:
if abs(median_deviation) < 1:
step = 0.02
if median_deviation > 0:
self.mpv.speed += step
else:
self.mpv.speed -= step
logger.error(
'%0.05f need to adjust speed %0.05f (%d) median %0.05f (%d) -> %s' % (self.mpv.time_pos, deviation, frames, median_deviation, median_frames, self.mpv.speed)
)
self.need_to_sync = False
self.deviations = collections.deque(maxlen=10)
self.last_sync = time.time()
elif self.mpv.time_pos > 2 and not self.need_to_sync:
2023-11-17 00:05:50 +00:00
logger.error(
'%0.05f need to sync %0.05f (%d) median %0.05f (%d)' % (self.mpv.time_pos, deviation, frames, median_deviation, median_frames)
)
2023-11-14 11:26:33 +00:00
self.need_to_sync = True
2023-10-30 19:26:11 +00:00
def median(self, lst):
quotient, remainder = divmod(len(lst), 2)
if remainder:
return sorted(lst)[quotient]
return float(sum(sorted(lst)[quotient - 1:quotient + 1]) / 2.0)
def sync_to_main(self):
2023-11-14 15:32:27 +00:00
logger.error('sync to main')
2023-10-30 19:26:11 +00:00
self.read_position_main()
#print(self.main.playlist_current_pos)
if self.main.playlist_current_pos != self.mpv.playlist_current_pos:
self.mpv.playlist_play_index(self.main.playlist_current_pos)
self.mpv.pause = False
self.mpv.wait_until_playing()
2023-11-17 00:13:28 +00:00
try:
track = self.mpv.playlist[self.mpv.playlist_current_pos]
logger.error("%s %s", datetime.now(), track["filename"])
except:
pass
2023-10-30 19:26:11 +00:00
self.mpv.pause = True
self.mpv.speed = 1
2023-10-30 19:26:11 +00:00
pos = self.main.time_pos + SYNC_JUMP_AHEAD
#print(pos, self.mpv.playlist_current_pos, self.mpv.time_pos)
self.mpv.seek(pos, 'absolute', 'exact')
time.sleep(0.1)
self.read_position_main()
sync_timer = time.time() # - 10 * 0.04
deviation = self.main.time_pos - self.mpv.time_pos
while self.active:
#print(deviation, abs(deviation) - (time.time() - sync_timer))
if abs(deviation) - (time.time() - sync_timer) < 0:
self.mpv.pause = False
2023-11-17 00:13:28 +00:00
try:
track = self.mpv.playlist[self.mpv.playlist_current_pos]
logger.error("%s %s %s", datetime.now(), track["filename"], pos)
except:
pass
2023-10-30 19:26:11 +00:00
break
self.last_sync = time.time()
def main():
prefix = os.path.expanduser('~/Videos/t_for_time')
parser = argparse.ArgumentParser(description='t_for_time sync player')
2024-01-22 14:06:40 +00:00
parser.add_argument('--mode', help='peer or main', default="peer")
2023-10-30 19:26:11 +00:00
parser.add_argument('--playlist', default='/srv/t_for_time/render/128/front.m3u', help="m3u")
parser.add_argument('--prefix', help='video location', default=prefix)
parser.add_argument('--window', action='store_true', help='run in window', default=False)
parser.add_argument('--debug', action='store_true', help='debug', default=False)
parser.add_argument('--hour', action='store_true', help='hour', default=False)
parser.add_argument('--sax', action='store_true', help='hour', default=False)
2024-04-01 10:08:21 +00:00
parser.add_argument('--config', help='config', default=None)
2023-10-30 19:26:11 +00:00
args = parser.parse_args()
DEBUG = args.debug
if DEBUG:
log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
logging.basicConfig(level=logging.DEBUG, format=log_format)
2024-04-01 10:08:21 +00:00
if args.config:
with open(args.config) as fd:
CONFIG.update(json.load(fd))
2023-10-30 19:26:11 +00:00
base = os.path.dirname(os.path.abspath(__file__))
#os.chdir(base)
player = Sync(mode=args.mode, playlist=args.playlist, fullscreen=not args.window, hour=args.hour, sax=args.sax)
2023-10-30 19:26:11 +00:00
while player.active:
2023-11-15 10:01:51 +00:00
try:
player.mpv.wait_for_playback()
except:
break
2023-10-30 19:26:11 +00:00
player.stop()
if __name__ == "__main__":
2023-11-20 23:27:49 +00:00
try:
hide_gnome_overview()
except:
pass
2023-10-30 19:26:11 +00:00
main()