609 lines
18 KiB
Python
Executable File
609 lines
18 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""
|
|
sync_mpv_client.py
|
|
|
|
Copyright 2022, 2023 <midnightman@protonmail.com>
|
|
Copyright 2023, Jeff Moe <moe@spacecruft.org>
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
"""
|
|
|
|
from Cryptodome.Cipher import AES
|
|
from Cryptodome.Util.Padding import pad, unpad
|
|
from python_mpv_jsonipc import MPV
|
|
from configparser import ConfigParser
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import errno
|
|
import socket
|
|
import hashlib
|
|
import datetime
|
|
import threading
|
|
import argparse
|
|
|
|
global connected
|
|
global mpv
|
|
|
|
|
|
def receive_message(client_socket):
|
|
try:
|
|
message_header = client_socket.recv(HEADER_LENGTH)
|
|
message_length = int(message_header)
|
|
|
|
except:
|
|
return "Reading Error", "Server"
|
|
|
|
msg = client_socket.recv(message_length)
|
|
|
|
decrypted_msg = decrypt_message(msg[16:], msg[:16])
|
|
|
|
return_list = decrypted_msg.split(" , ")
|
|
if len(return_list) > 1:
|
|
msg = return_list[0]
|
|
user = return_list[1]
|
|
|
|
return msg, user
|
|
|
|
else:
|
|
return decrypted_msg, "Server"
|
|
|
|
|
|
def pause_video(mpv):
|
|
mpv.command("set_property", "pause", True)
|
|
|
|
|
|
def play_video(mpv):
|
|
mpv.command("set_property", "pause", False)
|
|
|
|
|
|
def toggle_play(mpv):
|
|
isPaused = mpv.command("get_property", "pause")
|
|
if isPaused == True:
|
|
play_video(mpv)
|
|
else:
|
|
pause_video(mpv)
|
|
|
|
|
|
def new_video(mpv, new_link):
|
|
global t_playback
|
|
if new_link is not None:
|
|
mpv.play(new_link)
|
|
t_playback = 0
|
|
|
|
|
|
def send(clientsocket, msg):
|
|
global KEY
|
|
cipher = AES.new(KEY, AES.MODE_CBC)
|
|
if msg:
|
|
if type(msg) is not bytes:
|
|
msg = msg.encode("utf-8")
|
|
|
|
msg = encrypt_message(msg)
|
|
send_length = prepare_concatenation(len(msg))
|
|
|
|
clientsocket.send(send_length)
|
|
clientsocket.send(msg)
|
|
|
|
|
|
def encrypt_message(msg):
|
|
global KEY
|
|
|
|
cipher = AES.new(KEY, AES.MODE_CBC)
|
|
|
|
encrypted_msg = cipher.encrypt(pad(msg, AES.block_size))
|
|
encrypted_msg = cipher.iv + encrypted_msg
|
|
|
|
return encrypted_msg
|
|
|
|
|
|
def decrypt_message(msg, IV):
|
|
global KEY
|
|
cipher = AES.new(KEY, AES.MODE_CBC, IV)
|
|
decrypted_msg = unpad(cipher.decrypt(msg), AES.block_size)
|
|
|
|
try:
|
|
decrypted_msg = decrypted_msg.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
pass
|
|
|
|
return decrypted_msg
|
|
|
|
|
|
def prepare_concatenation(msg):
|
|
global HEADER_LENGTH
|
|
concat = str(msg).encode("utf-8")
|
|
concat += b" " * (HEADER_LENGTH - len(concat))
|
|
return concat
|
|
|
|
|
|
def ready_when_seeked(mpv, value):
|
|
while True:
|
|
seek_bool = mpv.command("get_property", "seeking")
|
|
if seek_bool == False:
|
|
pause_video(mpv)
|
|
send(client_socket, f"ready {value}")
|
|
break
|
|
|
|
|
|
def exit_gracefully():
|
|
send(client_socket, "!DISCONNECT")
|
|
|
|
|
|
def handle_server(server, addr, MPV_PATH):
|
|
t_playback = 0
|
|
connected = True
|
|
|
|
operating_system_used = sys.platform
|
|
|
|
if operating_system_used == "win32":
|
|
while True:
|
|
try:
|
|
mpv = MPV(
|
|
start_mpv=True, mpv_location=MPV_PATH, quit_callback=exit_gracefully
|
|
)
|
|
break
|
|
except FileNotFoundError:
|
|
MPV_PATH = input(
|
|
"mpv binary not found. Please enter the correct path to mpv.exe : "
|
|
).strip('"')
|
|
parser = ConfigParser()
|
|
parser.read(os.getenv("APPDATA") + "/sync-mpv/sync-mpv.conf")
|
|
parser.set("connection", "mpv_path", MPV_PATH)
|
|
with open(
|
|
os.getenv("APPDATA") + "/sync-mpv/sync-mpv.conf", "w"
|
|
) as configfile:
|
|
parser.write(configfile)
|
|
else:
|
|
mpv = MPV(start_mpv=True, quit_callback=exit_gracefully)
|
|
mpv.command("set_property", "keep-open", True)
|
|
mpv.command("set_property", "osd-font-size", "18")
|
|
|
|
# observe playback-time to synchronize when user skips on timeline
|
|
|
|
@mpv.property_observer("playback-time")
|
|
def observe_playback_time(name, value):
|
|
global t_playback
|
|
if value is not None:
|
|
if value > t_playback + 0.25 or value < t_playback - 0.1:
|
|
if f"mpv skip {value}" != msg:
|
|
t_playback = mpv.command("get_property", "playback-time")
|
|
print(t_playback)
|
|
send(client_socket, f"mpv skip {t_playback}")
|
|
ready_thread = threading.Thread(
|
|
target=ready_when_seeked, args=(mpv, value)
|
|
)
|
|
ready_thread.start()
|
|
|
|
t_playback = value
|
|
|
|
# observe path to distribute new video url to other clients
|
|
|
|
@mpv.property_observer("path")
|
|
def observe_path(name, value):
|
|
print(name, value)
|
|
if value is not None:
|
|
print(f"New Path: {value}")
|
|
print(msg)
|
|
if f"mpv new {value}" != msg:
|
|
send(client_socket, f"mpv new {value}")
|
|
new_video(mpv, value)
|
|
print("READY - PATH OBSERVED")
|
|
ready_thread = threading.Thread(
|
|
target=ready_when_seeked, args=(mpv, value)
|
|
)
|
|
ready_thread.start()
|
|
|
|
@mpv.property_observer("paused-for-cache")
|
|
def resync_on_cache(name, value):
|
|
print(name, value)
|
|
if value == True:
|
|
pause_video(mpv)
|
|
time_pos = mpv.command("get_property", "time-pos")
|
|
print(time_pos)
|
|
send(client_socket, f"mpv skip {time_pos}")
|
|
send(client_socket, "paused-for-cache")
|
|
|
|
ready_thread = threading.Thread(
|
|
target=ready_when_seeked, args=(mpv, time_pos)
|
|
)
|
|
ready_thread.start()
|
|
|
|
# when space pressed inform other clients of play/pause
|
|
|
|
@mpv.on_key_press("SPACE")
|
|
def toggle_playback():
|
|
toggle_play(mpv)
|
|
send(client_socket, "toggle play")
|
|
|
|
@mpv.on_key_press("MBTN_RIGHT")
|
|
def toggle_playback():
|
|
toggle_play(mpv)
|
|
send(client_socket, "toggle play")
|
|
|
|
# when q is pressed exit gracefully
|
|
|
|
@mpv.on_key_press("q")
|
|
def terminate():
|
|
global connected
|
|
print("Q")
|
|
send(client_socket, "!DISCONNECT")
|
|
connected = False
|
|
|
|
@mpv.on_key_press(",")
|
|
def frame_back_step():
|
|
send(client_socket, "frame-back-step")
|
|
pause_video(mpv)
|
|
mpv.command("frame-back-step")
|
|
|
|
@mpv.on_key_press(".")
|
|
def frame_step():
|
|
send(client_socket, "frame-step")
|
|
mpv.command("frame-step")
|
|
|
|
@mpv.on_key_press("-")
|
|
def subtract_speed():
|
|
print("n")
|
|
send(client_socket, "subtract_speed")
|
|
mpv.command("add", "speed", -0.1)
|
|
speed = mpv.command("get_property", "speed")
|
|
mpv.command("show-text", f"Setting playback-speed to {speed}", "1500")
|
|
print("slowed down")
|
|
|
|
@mpv.on_key_press("+")
|
|
def add_speed():
|
|
print("")
|
|
send(client_socket, "add_speed")
|
|
mpv.command("add", "speed", 0.1)
|
|
speed = mpv.command("get_property", "speed")
|
|
mpv.command("show-text", f"Setting playback-speed to {speed}", "1500")
|
|
print("sped up")
|
|
|
|
@mpv.on_key_press("r")
|
|
def resync():
|
|
time_pos = mpv.command("get_property", "time-pos")
|
|
print(time_pos)
|
|
send(client_socket, f"mpv skip {time_pos}")
|
|
send(client_socket, "resync")
|
|
|
|
ready_thread = threading.Thread(target=ready_when_seeked, args=(mpv, time_pos))
|
|
ready_thread.start()
|
|
|
|
@mpv.on_key_press("h")
|
|
def help():
|
|
mpv.command(
|
|
"show-text",
|
|
"""sync-mpv keybindings
|
|
|
|
Space / Right Mousebutton Toggle Play/Pause
|
|
|
|
|
|
r resync clients
|
|
+/- Speed up/down the video
|
|
. Go one frame forwards
|
|
, Go one frame backwards
|
|
q Quit sync-mpv
|
|
|
|
Zoom:
|
|
KP 7 Zoom out
|
|
KP 9 Zoom in
|
|
KP 8 Move panorama up
|
|
KP 2 Move panorama down
|
|
KP 4 Move panorama left
|
|
KP 6 Move panorama right
|
|
|
|
KP 5 Deactivate zoom and go back to normal view mode.
|
|
""",
|
|
5000,
|
|
)
|
|
|
|
@mpv.on_key_press("kp7")
|
|
def printest():
|
|
send(client_socket, "zoom-out")
|
|
mpv.command("add", "video-zoom", "-.25")
|
|
|
|
@mpv.on_key_press("kp9")
|
|
def printest():
|
|
send(client_socket, "zoom-in")
|
|
mpv.command("add", "video-zoom", ".25")
|
|
|
|
@mpv.on_key_press("kp8")
|
|
def printest():
|
|
send(client_socket, "move-up")
|
|
mpv.command("add", "video-pan-y", ".05")
|
|
|
|
@mpv.on_key_press("kp2")
|
|
def printest():
|
|
send(client_socket, "move-down")
|
|
mpv.command("add", "video-pan-y", "-.05")
|
|
|
|
@mpv.on_key_press("kp4")
|
|
def printest():
|
|
send(client_socket, "move-left")
|
|
mpv.command("add", "video-pan-x", ".05")
|
|
|
|
@mpv.on_key_press("kp6")
|
|
def printest():
|
|
send(client_socket, "move-right")
|
|
mpv.command("add", "video-pan-x", "-.05")
|
|
|
|
@mpv.on_key_press("kp5")
|
|
def printest():
|
|
send(client_socket, "reset-window")
|
|
mpv.command("set", "video-pan-y", "0")
|
|
mpv.command("set", "video-pan-x", "0")
|
|
mpv.command("set", "video-zoom", "0")
|
|
|
|
print(f"[CONNECTION ESTABLISHED] to {addr}")
|
|
|
|
while connected:
|
|
try:
|
|
msg, user = receive_message(server)
|
|
|
|
if msg != False:
|
|
if msg == "!DISCONNECT":
|
|
connected = False
|
|
break
|
|
|
|
if msg == "zoom-in":
|
|
mpv.command("add", "video-zoom", ".25")
|
|
mpv.command("show-text", f"{user} zooms in.", "1500")
|
|
|
|
if msg == "zoom-out":
|
|
mpv.command("add", "video-zoom", "-.25")
|
|
mpv.command("show-text", f"{user} zooms out.", "1500")
|
|
|
|
if msg == "move-up":
|
|
mpv.command("add", "video-pan-y", ".05")
|
|
mpv.command("show-text", f"{user} zooms in.", "1500")
|
|
|
|
if msg == "move-down":
|
|
mpv.command("add", "video-pan-y", "-.05")
|
|
|
|
if msg == "move-right":
|
|
mpv.command("add", "video-pan-x", "-.05")
|
|
|
|
if msg == "move-left":
|
|
mpv.command("add", "video-pan-x", ".05")
|
|
|
|
if msg == "reset-window":
|
|
mpv.command("set", "video-pan-y", "0")
|
|
mpv.command("set", "video-pan-x", "0")
|
|
mpv.command("set", "video-zoom", "0")
|
|
mpv.command("show-text", f"{user} resets the window.", "1500")
|
|
|
|
if msg == "frame-step":
|
|
mpv.command("frame-step")
|
|
|
|
if msg == "add_speed":
|
|
mpv.command("add", "speed", 0.1)
|
|
speed = mpv.command("get_property", "speed")
|
|
mpv.command(
|
|
"show-text", f"{user} sets playback-speed to {speed}", "1500"
|
|
)
|
|
|
|
if msg == "subtract_speed":
|
|
mpv.command("add", "speed", -0.1)
|
|
speed = mpv.command("get_property", "speed")
|
|
mpv.command(
|
|
"show-text", f"{user} sets playback-speed to {speed}", "1500"
|
|
)
|
|
|
|
if msg == "frame-back-step":
|
|
mpv.command("frame-back-step")
|
|
|
|
if msg == "mpv pause":
|
|
pause_video(mpv)
|
|
|
|
if msg == "mpv terminate":
|
|
connected = False
|
|
|
|
if msg == "mpv playback":
|
|
play_video(mpv)
|
|
|
|
if "disconnected" in msg:
|
|
mpv.command("show-text", f"{msg}", "5000")
|
|
|
|
if msg == "number of clients":
|
|
number_of_clients = int(msg.split[" "][3])
|
|
print("Number of Clients: %s" % number_of_clients)
|
|
|
|
if msg == "resync":
|
|
mpv.command("show-text", f"{user} resyncs.", "1500")
|
|
|
|
if msg == "paused-for-cache":
|
|
if user.endswith("s"):
|
|
pass
|
|
else:
|
|
user = user + "s"
|
|
mpv.command(
|
|
"show-text",
|
|
f"{user} video paused for caching. Resyncing.",
|
|
"5000",
|
|
)
|
|
|
|
if msg == "toggle play":
|
|
toggle_play(mpv)
|
|
mpv.command("show-text", f"{user} toggles", "1500")
|
|
|
|
if "mpv skip" in msg:
|
|
t_playback = float(msg.split(" ")[2])
|
|
|
|
if t_playback < 3600:
|
|
converted_time = time.strftime("%M:%S", time.gmtime(t_playback))
|
|
else:
|
|
converted_time = time.strftime(
|
|
"%H:%M:%S", time.gmtime(t_playback)
|
|
)
|
|
|
|
mpv.command("set_property", "playback-time", msg.split(" ")[2])
|
|
mpv.command(
|
|
"show-text", f"{user} skips to {converted_time}", "1500"
|
|
)
|
|
|
|
ready_thread = threading.Thread(
|
|
target=ready_when_seeked, args=(mpv, t_playback)
|
|
)
|
|
ready_thread.start()
|
|
|
|
if "mpv new" in msg:
|
|
videopath = msg[8:]
|
|
new_video(mpv, videopath)
|
|
mpv.command("show-text", f"{user}: {videopath}", "1500")
|
|
|
|
ready_thread = threading.Thread(
|
|
target=ready_when_seeked, args=(mpv, videopath)
|
|
)
|
|
ready_thread.start()
|
|
|
|
if "userconnected" in msg:
|
|
mpv.command("show-text", f"{msg.split(' ')[1]} connected.", "1500")
|
|
|
|
except IOError as e:
|
|
if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
|
|
print(e)
|
|
print("Reading error: {}".format(str(e)))
|
|
continue
|
|
# sys.exit()
|
|
|
|
# We just did not receive anything
|
|
# break
|
|
|
|
except Exception as e:
|
|
# Any other exception - something happened
|
|
print(e)
|
|
print("Reading error: ".format(str(e)))
|
|
# sys.exit()
|
|
|
|
mpv.terminate()
|
|
client_socket.shutdown(socket.SHUT_RDWR)
|
|
client_socket.close()
|
|
|
|
|
|
def parse_config(parser, configfile):
|
|
operating_system_used = sys.platform
|
|
|
|
parser.read(configfile)
|
|
IP = parser.get("connection", "ip")
|
|
PORT = parser.getint("connection", "port")
|
|
USERNAME = parser.get("connection", "username")
|
|
PASSWORD = parser.get("connection", "password")
|
|
MPV_PATH = parser.get("connection", "mpv_path")
|
|
return IP, PORT, USERNAME, PASSWORD, MPV_PATH
|
|
|
|
|
|
def initialize(parser, configfile):
|
|
operating_system_used = sys.platform
|
|
IP = input("IP: ")
|
|
PASSWORD = input("Password: ")
|
|
USERNAME = input("Username: ")
|
|
if operating_system_used == "win32":
|
|
MPV_PATH = input("Path to mpv.exe : ").strip('"')
|
|
else:
|
|
MPV_PATH = "linux-binary"
|
|
parser["connection"] = {
|
|
"ip": IP,
|
|
"port": "51984",
|
|
"username": USERNAME,
|
|
"password": PASSWORD,
|
|
"mpv_path": MPV_PATH,
|
|
}
|
|
with open(configfile, "w") as f:
|
|
parser.write(f)
|
|
|
|
|
|
def main():
|
|
global KEY
|
|
global HEADER_LENGTH
|
|
global client_socket
|
|
|
|
HEADER_LENGTH = 32
|
|
FORMAT = "utf-8"
|
|
DISCONNECT_MESSAGE = "!DISCONNECT"
|
|
|
|
operating_system_used = sys.platform
|
|
|
|
if operating_system_used == "win32":
|
|
configfolder = os.getenv("APPDATA") + "/sync-mpv/"
|
|
else:
|
|
configfolder = os.path.expanduser("~/.config/sync-mpv/")
|
|
|
|
configfile = configfolder + "sync-mpv.conf"
|
|
|
|
parser = ConfigParser()
|
|
|
|
if os.path.exists(configfolder):
|
|
pass
|
|
else:
|
|
os.mkdir(configfolder)
|
|
|
|
if not os.path.exists(configfile):
|
|
initialize(parser, configfile)
|
|
|
|
IP, PORT, USERNAME, PASSWORD, MPV_PATH = parse_config(parser, configfile)
|
|
|
|
# Parse command line options
|
|
parser = argparse.ArgumentParser(description="mpv synchronization client")
|
|
parser.add_argument(
|
|
"-i",
|
|
"--ip",
|
|
help="Server IP address",
|
|
type=str,
|
|
required=False,
|
|
default=IP,
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--port",
|
|
help="Server Port",
|
|
type=int,
|
|
required=False,
|
|
default=PORT,
|
|
)
|
|
args = parser.parse_args()
|
|
IP = args.ip
|
|
PORT = args.port
|
|
|
|
KEY = hashlib.sha256(PASSWORD.encode()).digest()
|
|
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
# Connect to a given ip and port
|
|
while True:
|
|
try:
|
|
client_socket.connect((IP, PORT))
|
|
break
|
|
except ConnectionRefusedError:
|
|
print("\nEnter new IP if server IP has changed.\nLeave blank otherwise.\n")
|
|
|
|
IP = input("IP: ")
|
|
if IP == "":
|
|
pass
|
|
|
|
config = ConfigParser()
|
|
config.read(configfile)
|
|
config.set("connection", "ip", "%s" % IP)
|
|
|
|
with open(configfile, "w") as f:
|
|
config.write(f)
|
|
|
|
client_socket.setblocking(True)
|
|
|
|
send(client_socket, USERNAME)
|
|
handle_server(client_socket, (IP, PORT), MPV_PATH)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|