diff --git a/cdosea-play b/cdosea-play index ac59d32..b243b61 100755 --- a/cdosea-play +++ b/cdosea-play @@ -2,7 +2,6 @@ if [ "x$1" == "xupdate" ]; then shift exec python3 -m cdoseaplay.update $@ - else shift gsettings set org.gnome.desktop.screensaver ubuntu-lock-on-suspend false @@ -14,5 +13,9 @@ else gsettings set org.gnome.desktop.background primary-color '#888888' gsettings set org.gnome.desktop.background secondary-color '#888888' gsettings set org.gnome.desktop.background picture-options 'none' - exec python3 -m cdoseaplay.play $@ + if [ "x$1" == "xsync" ]; then + exec python3 -m cdoseaplay.sync $@ + else + exec python3 -m cdoseaplay.play $@ + fi fi diff --git a/cdoseaplay/lights.py b/cdoseaplay/lights.py new file mode 100755 index 0000000..96f8e5f --- /dev/null +++ b/cdoseaplay/lights.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +import subprocess +import sys +import os +import time + +import ox +from pi import random + +path = sys.argv[1] + +n = int(os.path.getctime(path) - 1495280000) +duration = ox.avinfo(path)['duration'] + +seq = random(n * 1000) +pos = 0 +lights = [] +while pos < duration - 15: + sleep = seq() + 15 + light = seq() + 1 + if pos + sleep > duration: + break + time.sleep(sleep) + cmd = ['/opt/LanBox-JSONRPC/fade.py', str(light)] + subprocess.Popen(cmd) + pos += sleep diff --git a/cdoseaplay/sync.py b/cdoseaplay/sync.py new file mode 100755 index 0000000..e139c6f --- /dev/null +++ b/cdoseaplay/sync.py @@ -0,0 +1,212 @@ +#!/usr/bin/python3 +import argparse +import datetime +import os +import random +import socket +import string +import subprocess +import sys +import time +from glob import glob +from queue import Queue +from socketserver import UDPServer, ThreadingMixIn, BaseRequestHandler +from threading import Thread + +import ox +import mpv + +import logging +logger = logging.getLogger('cdosea') + +DEFAULT_PORT = 2680 +DEBUG = False + +def update_playlist(playlist, prefix='video/', position=None): + playlist = os.path.expanduser('~/Videos/cdosea.m3u') + prefix = os.path.expanduser('~/Videos/CDOSEA') + + files = [] + videos = {} + for letter in string.ascii_uppercase: + videos[letter] = glob('%s%s*.mp4' % (prefix, letter.lower())) + random.shuffle(videos[letter]) + + for i in range(10): + for letter in string.ascii_uppercase: + files.append(videos[letter][i]) + + if position is None: + today = datetime.date.today() + seconds_since_midnight = time.time() - time.mktime(today.timetuple()) + offset = 0 + position = 0 + + while offset < seconds_since_midnight: + f = files.pop(0) + try: + offset += ox.avinfo(f)['duration'] + files.append(f) + except: + pass + position += 1 + else: + pos = position + while pos: + f = files.pop(0) + files.append(f) + pos -= 1 + + with open(playlist, 'w') as f: + f.write('\n'.join(files)) + f.write('\n') + return position, len(files) + +def trigger_lights(path): + cmd = ['python3', '-m', 'cdoseaplay.lights', path] + subprocess.Popen(cmd) + +class ThreadingUDPServer(ThreadingMixIn, UDPServer): + pass + + def q_binding(self, *args): + self.player.running = False + self.shutdown() + +class Handler(BaseRequestHandler): + + def handle(self): + data = self.request[0].strip() + logger.debug("%s said: %s", self.client_address[0], data) + queue = False + if data == b'POS': + response = ('%d' % self.server.player.position).encode() + else: + if self.server.player.playing: + response = b'PLAYING' + else: + response = b'WAITING' + queue = True + socket = self.request[1] + socket.sendto(response, self.client_address) + if queue: + self.server.player.queue.put('') + +class Player(Thread): + first = True + running = True + playing = False + current = '' + playlist_items = 260 + + def __init__(self, peer, player, playlist, lights=False): + self.peer = peer + self.playlist = playlist + self.player = player + self.lights = lights + self.queue = Queue() + Thread.__init__(self) + self.daemon = True + self.start() + + def run(self): + logger.debug("update playlist") + self.position, self.playlist_items = update_playlist(self.playlist, position=self.get_position()) + logger.debug("load playlist") + self.player.loadlist(self.playlist) + self.player.pause = True + while self.running: + logger.debug("play") + self.play() + self.position = (self.position + 1) % self.playlist_items + logger.debug("ping") + self.ping() + logger.debug("wait") + self.queue.get() + + def play(self): + self.playing = True + self.player.pause = False + if self.lights: + trigger_lights(self.player.path.decode()) + self.player.wait_for_playback() + self.player.pause = True + self.playing = False + + def ping(self): + data = 'NEXT' + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + logger.debug("ping: %s %s", self.peer, data) + sock.sendto(bytes(data + "\n", "utf-8"), self.peer) + received = str(sock.recv(1024), "utf-8") + if received == 'WAITING' and not self.queue.qsize(): + if self.first: + self.queue.put('') + self.first = False + logger.debug("Sent: %s", data) + logger.debug("Received: %s", received) + + def get_position(self): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(1) + data = 'POS' + sock.sendto(bytes(data + "\n", "utf-8"), self.peer) + received = str(sock.recv(1024), "utf-8") + pos = int(received) + except socket.timeout: + logger.debug('no response', exc_info=1) + pos = None + return pos + + +def my_log(loglevel, component, message): + logger.debug('[{}] {}: {}'.format(loglevel, component, message)) + +def main(): + parser = argparse.ArgumentParser(description='play 2 screens in sync') + parser.add_argument('--peer', help='ip[:port] of peer', required=True) + parser.add_argument('--port', type=int, help='local port', default=DEFAULT_PORT) + parser.add_argument('--playlist', help='play.m3u', default='play.m3u') + 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('--lights', action='store_true', help='lights', default=False) + args = parser.parse_args() + + if ':' in args.peer: + peer = args.peer.split(':') + peer = (peer[0], int(peer[1])) + else: + peer = (args.peer, DEFAULT_PORT) + + DEBUG = args.debug + if DEBUG: + log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' + logging.basicConfig(level=logging.DEBUG, format=log_format) + base = os.path.dirname(os.path.abspath(__file__)) + os.chdir(base) + if args.lights: + time.sleep(5) + + # 42 max + player = mpv.MPV( + log_handler=my_log, input_default_bindings=True, + input_vo_keyboard=True, sub_text_font_size=28, sub_text_font='Menlo' + ) + player.fullscreen = not args.window + player.loop = 'inf' + + server = ThreadingUDPServer(('0.0.0.0', args.port), Handler) + server.player = Player(peer, player, args.playlist, args.lights) + player.register_key_binding('q', server.q_binding) + + logger.debug("listen on %s...", args.port) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + del player + + +if __name__ == "__main__": + main()