sync-mpv/sync_mpv_client.py

609 lines
18 KiB
Python
Raw Permalink Normal View History

2022-04-08 09:20:44 -06:00
#!/usr/bin/env python
2023-09-14 10:46:17 -06:00
"""
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.
"""
2023-09-13 14:21:19 -06:00
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
2022-04-08 09:20:44 -06:00
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
2023-09-14 11:25:44 -06:00
import argparse
2022-04-08 09:20:44 -06:00
global connected
global mpv
2023-09-14 10:51:07 -06:00
def receive_message(client_socket):
2022-04-08 09:20:44 -06:00
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"
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def pause_video(mpv):
2023-09-14 10:51:07 -06:00
mpv.command("set_property", "pause", True)
2022-04-08 09:20:44 -06:00
def play_video(mpv):
2023-09-14 10:51:07 -06:00
mpv.command("set_property", "pause", False)
2022-04-08 09:20:44 -06:00
def toggle_play(mpv):
2023-09-14 10:51:07 -06:00
isPaused = mpv.command("get_property", "pause")
2022-04-08 09:20:44 -06:00
if isPaused == True:
play_video(mpv)
else:
pause_video(mpv)
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def new_video(mpv, new_link):
global t_playback
if new_link is not None:
mpv.play(new_link)
t_playback = 0
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def send(clientsocket, msg):
global KEY
cipher = AES.new(KEY, AES.MODE_CBC)
if msg:
if type(msg) is not bytes:
2023-09-14 10:51:07 -06:00
msg = msg.encode("utf-8")
2022-04-08 09:20:44 -06:00
msg = encrypt_message(msg)
send_length = prepare_concatenation(len(msg))
clientsocket.send(send_length)
clientsocket.send(msg)
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def encrypt_message(msg):
global KEY
cipher = AES.new(KEY, AES.MODE_CBC)
2023-09-14 10:51:07 -06:00
encrypted_msg = cipher.encrypt(pad(msg, AES.block_size))
2022-04-08 09:20:44 -06:00
encrypted_msg = cipher.iv + encrypted_msg
return encrypted_msg
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
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
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def prepare_concatenation(msg):
global HEADER_LENGTH
concat = str(msg).encode("utf-8")
2023-09-14 10:51:07 -06:00
concat += b" " * (HEADER_LENGTH - len(concat))
2022-04-08 09:20:44 -06:00
return concat
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def ready_when_seeked(mpv, value):
while True:
2023-09-14 10:51:07 -06:00
seek_bool = mpv.command("get_property", "seeking")
2022-04-08 09:20:44 -06:00
if seek_bool == False:
pause_video(mpv)
send(client_socket, f"ready {value}")
break
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def exit_gracefully():
send(client_socket, "!DISCONNECT")
2023-09-14 10:51:07 -06:00
def handle_server(server, addr, MPV_PATH):
2022-04-08 09:20:44 -06:00
t_playback = 0
connected = True
operating_system_used = sys.platform
if operating_system_used == "win32":
while True:
try:
2023-09-14 10:51:07 -06:00
mpv = MPV(
start_mpv=True, mpv_location=MPV_PATH, quit_callback=exit_gracefully
)
break
except FileNotFoundError:
2023-09-14 10:51:07 -06:00
MPV_PATH = input(
"mpv binary not found. Please enter the correct path to mpv.exe : "
).strip('"')
parser = ConfigParser()
2023-09-14 10:51:07 -06:00
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)
2023-09-14 10:51:07 -06:00
mpv.command("set_property", "keep-open", True)
mpv.command("set_property", "osd-font-size", "18")
2022-04-08 09:20:44 -06:00
2023-09-14 10:51:07 -06:00
# observe playback-time to synchronize when user skips on timeline
2022-04-08 09:20:44 -06:00
@mpv.property_observer("playback-time")
2023-09-14 10:51:07 -06:00
def observe_playback_time(name, value):
global t_playback
2022-04-08 09:20:44 -06:00
if value is not None:
2023-09-14 10:51:07 -06:00
if value > t_playback + 0.25 or value < t_playback - 0.1:
2022-04-08 09:20:44 -06:00
if f"mpv skip {value}" != msg:
t_playback = mpv.command("get_property", "playback-time")
2023-09-14 10:51:07 -06:00
print(t_playback)
2022-04-08 09:20:44 -06:00
send(client_socket, f"mpv skip {t_playback}")
2023-09-14 10:51:07 -06:00
ready_thread = threading.Thread(
target=ready_when_seeked, args=(mpv, value)
)
ready_thread.start()
2022-04-08 09:20:44 -06:00
t_playback = value
2023-09-14 10:51:07 -06:00
# observe path to distribute new video url to other clients
2022-04-08 09:20:44 -06:00
@mpv.property_observer("path")
def observe_path(name, value):
print(name, value)
if value is not None:
2023-09-14 10:51:07 -06:00
print(f"New Path: {value}")
2022-04-08 09:20:44 -06:00
print(msg)
2023-09-14 10:51:07 -06:00
if f"mpv new {value}" != msg:
2022-04-08 09:20:44 -06:00
send(client_socket, f"mpv new {value}")
new_video(mpv, value)
print("READY - PATH OBSERVED")
2023-09-14 10:51:07 -06:00
ready_thread = threading.Thread(
target=ready_when_seeked, args=(mpv, value)
)
ready_thread.start()
2022-04-08 09:20:44 -06:00
@mpv.property_observer("paused-for-cache")
2023-02-24 13:41:37 -07:00
def resync_on_cache(name, value):
print(name, value)
if value == True:
pause_video(mpv)
2023-09-14 10:51:07 -06:00
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")
2023-09-14 10:51:07 -06:00
ready_thread = threading.Thread(
target=ready_when_seeked, args=(mpv, time_pos)
)
ready_thread.start()
2022-04-08 09:20:44 -06:00
# when space pressed inform other clients of play/pause
@mpv.on_key_press("SPACE")
2022-04-08 09:20:44 -06:00
def toggle_playback():
toggle_play(mpv)
send(client_socket, "toggle play")
2023-09-14 10:51:07 -06:00
@mpv.on_key_press("MBTN_RIGHT")
def toggle_playback():
toggle_play(mpv)
2023-09-14 10:51:07 -06:00
send(client_socket, "toggle play")
2022-04-08 09:20:44 -06:00
# 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)
2023-09-14 10:51:07 -06:00
mpv.command("frame-back-step")
2022-04-08 09:20:44 -06:00
@mpv.on_key_press(".")
def frame_step():
send(client_socket, "frame-step")
2023-09-14 10:51:07 -06:00
mpv.command("frame-step")
2022-04-08 09:20:44 -06:00
@mpv.on_key_press("-")
def subtract_speed():
print("n")
send(client_socket, "subtract_speed")
2023-09-14 10:51:07 -06:00
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")
2022-04-08 09:20:44 -06:00
@mpv.on_key_press("+")
def add_speed():
print("")
send(client_socket, "add_speed")
2023-09-14 10:51:07 -06:00
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")
2022-04-08 09:20:44 -06:00
@mpv.on_key_press("r")
def resync():
2023-09-14 10:51:07 -06:00
time_pos = mpv.command("get_property", "time-pos")
2022-04-08 09:20:44 -06:00
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():
2023-09-14 10:51:07 -06:00
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.
2023-09-14 10:51:07 -06:00
""",
5000,
)
@mpv.on_key_press("kp7")
def printest():
send(client_socket, "zoom-out")
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-zoom", "-.25")
@mpv.on_key_press("kp9")
def printest():
send(client_socket, "zoom-in")
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-zoom", ".25")
@mpv.on_key_press("kp8")
def printest():
send(client_socket, "move-up")
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-y", ".05")
@mpv.on_key_press("kp2")
def printest():
send(client_socket, "move-down")
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-y", "-.05")
@mpv.on_key_press("kp4")
def printest():
send(client_socket, "move-left")
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-x", ".05")
@mpv.on_key_press("kp6")
def printest():
send(client_socket, "move-right")
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-x", "-.05")
@mpv.on_key_press("kp5")
def printest():
send(client_socket, "reset-window")
2023-09-14 10:51:07 -06:00
mpv.command("set", "video-pan-y", "0")
mpv.command("set", "video-pan-x", "0")
mpv.command("set", "video-zoom", "0")
2022-04-08 09:20:44 -06:00
print(f"[CONNECTION ESTABLISHED] to {addr}")
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
while connected:
try:
msg, user = receive_message(server)
if msg != False:
if msg == "!DISCONNECT":
connected = False
break
if msg == "zoom-in":
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-zoom", ".25")
mpv.command("show-text", f"{user} zooms in.", "1500")
if msg == "zoom-out":
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-zoom", "-.25")
mpv.command("show-text", f"{user} zooms out.", "1500")
if msg == "move-up":
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-y", ".05")
mpv.command("show-text", f"{user} zooms in.", "1500")
if msg == "move-down":
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-y", "-.05")
if msg == "move-right":
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-x", "-.05")
if msg == "move-left":
2023-09-14 10:51:07 -06:00
mpv.command("add", "video-pan-x", ".05")
if msg == "reset-window":
2023-09-14 10:51:07 -06:00
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")
2022-04-08 09:20:44 -06:00
if msg == "frame-step":
2023-09-14 10:51:07 -06:00
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"
)
2022-04-08 09:20:44 -06:00
if msg == "frame-back-step":
2023-09-14 10:51:07 -06:00
mpv.command("frame-back-step")
2022-04-08 09:20:44 -06:00
if msg == "mpv pause":
pause_video(mpv)
if msg == "mpv terminate":
connected = False
if msg == "mpv playback":
play_video(mpv)
if "disconnected" in msg:
2023-09-14 10:51:07 -06:00
mpv.command("show-text", f"{msg}", "5000")
2022-04-08 09:20:44 -06:00
if msg == "number of clients":
number_of_clients = int(msg.split[" "][3])
2023-09-14 10:51:07 -06:00
print("Number of Clients: %s" % number_of_clients)
2022-04-08 09:20:44 -06:00
if msg == "resync":
2023-09-14 10:51:07 -06:00
mpv.command("show-text", f"{user} resyncs.", "1500")
if msg == "paused-for-cache":
if user.endswith("s"):
pass
else:
2023-09-14 10:51:07 -06:00
user = user + "s"
mpv.command(
"show-text",
f"{user} video paused for caching. Resyncing.",
"5000",
)
2022-04-08 09:20:44 -06:00
if msg == "toggle play":
toggle_play(mpv)
2023-09-14 10:51:07 -06:00
mpv.command("show-text", f"{user} toggles", "1500")
2022-04-08 09:20:44 -06:00
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:
2023-09-14 10:51:07 -06:00
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()
2022-04-08 09:20:44 -06:00
if "mpv new" in msg:
videopath = msg[8:]
new_video(mpv, videopath)
2023-09-14 10:51:07 -06:00
mpv.command("show-text", f"{user}: {videopath}", "1500")
ready_thread = threading.Thread(
target=ready_when_seeked, args=(mpv, videopath)
)
ready_thread.start()
2022-04-08 09:20:44 -06:00
if "userconnected" in msg:
2023-09-14 10:51:07 -06:00
mpv.command("show-text", f"{msg.split(' ')[1]} connected.", "1500")
2022-04-08 09:20:44 -06:00
except IOError as e:
if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
print(e)
2023-09-14 10:51:07 -06:00
print("Reading error: {}".format(str(e)))
2022-04-08 09:20:44 -06:00
continue
2023-09-14 10:51:07 -06:00
# sys.exit()
2022-04-08 09:20:44 -06:00
# We just did not receive anything
2023-09-14 10:51:07 -06:00
# break
2022-04-08 09:20:44 -06:00
except Exception as e:
# Any other exception - something happened
print(e)
2023-09-14 10:51:07 -06:00
print("Reading error: ".format(str(e)))
# sys.exit()
2022-04-08 09:20:44 -06:00
mpv.terminate()
client_socket.shutdown(socket.SHUT_RDWR)
client_socket.close()
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def parse_config(parser, configfile):
operating_system_used = sys.platform
2022-04-08 09:20:44 -06:00
parser.read(configfile)
2023-09-14 10:51:07 -06:00
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
2022-04-08 09:20:44 -06:00
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def initialize(parser, configfile):
operating_system_used = sys.platform
2022-04-08 09:20:44 -06:00
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"
2023-09-14 10:51:07 -06:00
parser["connection"] = {
"ip": IP,
"port": "51984",
"username": USERNAME,
"password": PASSWORD,
"mpv_path": MPV_PATH,
2022-04-08 09:20:44 -06:00
}
2023-09-14 10:51:07 -06:00
with open(configfile, "w") as f:
2022-04-08 09:20:44 -06:00
parser.write(f)
2023-09-14 10:51:07 -06:00
def main():
2022-04-08 09:20:44 -06:00
global KEY
global HEADER_LENGTH
global client_socket
HEADER_LENGTH = 32
2023-09-14 10:51:07 -06:00
FORMAT = "utf-8"
DISCONNECT_MESSAGE = "!DISCONNECT"
operating_system_used = sys.platform
2022-04-08 09:20:44 -06:00
if operating_system_used == "win32":
2023-09-14 10:51:07 -06:00
configfolder = os.getenv("APPDATA") + "/sync-mpv/"
else:
configfolder = os.path.expanduser("~/.config/sync-mpv/")
2023-09-14 10:51:07 -06:00
configfile = configfolder + "sync-mpv.conf"
2022-04-08 09:20:44 -06:00
parser = ConfigParser()
if os.path.exists(configfolder):
pass
else:
os.mkdir(configfolder)
if not os.path.exists(configfile):
2022-04-08 09:20:44 -06:00
initialize(parser, configfile)
IP, PORT, USERNAME, PASSWORD, MPV_PATH = parse_config(parser, configfile)
2023-09-14 10:51:07 -06:00
2023-09-14 11:25:44 -06:00
# 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
2022-04-08 09:20:44 -06:00
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)
2023-09-14 10:51:07 -06:00
config.set("connection", "ip", "%s" % IP)
2022-04-08 09:20:44 -06:00
with open(configfile, "w") as f:
config.write(f)
client_socket.setblocking(True)
send(client_socket, USERNAME)
2023-09-14 10:51:07 -06:00
handle_server(client_socket, (IP, PORT), MPV_PATH)
2022-04-08 09:20:44 -06:00
if __name__ == "__main__":
main()