diff --git a/web/custom_widgets/PlyrVideoPlayer.py b/web/custom_widgets/PlyrVideoPlayer.py
new file mode 100644
index 0000000..125fbe8
--- /dev/null
+++ b/web/custom_widgets/PlyrVideoPlayer.py
@@ -0,0 +1,118 @@
+import uuid
+
+from nicegui import ui
+
+
+class PlyrVideoPlayer:
+ _plyr_installed = False
+
+ def __init__(self, src: str, poster_url: str = None):
+ if not PlyrVideoPlayer._plyr_installed:
+ install_plyr()
+ PlyrVideoPlayer._plyr_installed = True
+
+ self.src = src
+ self.poster_url = poster_url
+
+ self.element_id = f"plyr_{uuid.uuid4().hex}"
+ self.player_var = f"player_{self.element_id}"
+ self.event_play = f"{self.element_id}_playing"
+ self.event_pause = f"{self.element_id}_pause"
+ self.event_end = f"{self.element_id}_ended"
+ self.event_seeked = f"{self.element_id}_seeked"
+
+ poster_attr = f"data-poster=\"{poster_url}\"" if poster_url else ""
+ source_html = f""
+ video_html = (
+ f""
+ )
+
+ with ui.element("div").classes("plyr-container"):
+ ui.html(video_html)
+
+ options = {}
+ options.setdefault("settings", [])
+ options.setdefault("ratio", "16:9")
+ options.setdefault("controls",
+ ["play-large", "play", "rewind", "fast-forward", "current-time", "progress", "duration",
+ "mute", "volume", "pip", "airplay", "download", "fullscreen"]
+ )
+ js_options = str(options).replace("True", "true").replace("'", '"')
+
+ js = f"""
+ (async () => {{
+ const video = document.getElementById('{self.element_id}');
+ const player = new Plyr(video, {js_options});
+ window.{self.player_var} = player;
+
+ player.on('play', () => emitEvent('{self.event_play}'));
+ player.on('pause', () => emitEvent('{self.event_pause}'));
+ player.on('ended', () => emitEvent('{self.event_end}'));
+ player.on('seeked', () => emitEvent('{self.event_seeked}'));
+ }})();
+ """
+ ui.run_javascript(js)
+
+ def on(self, event: str, callback: callable):
+ mapping = {
+ "play": self.event_play,
+ "pause": self.event_pause,
+ "end": self.event_end,
+ "seeked": self.event_seeked
+ }
+ if event not in mapping:
+ raise ValueError("Supported events: 'play', 'pause', 'end', 'seeked'")
+ ui.on(mapping[event], callback)
+
+ async def is_seeking(self):
+ return await ui.run_javascript(f"window.{self.player_var}.seeking")
+
+ async def get_audio_tracks(self):
+ return await ui.run_javascript(f"window.{self.player_var}.audio_tracks")
+
+ def play(self):
+ ui.run_javascript(f"window.{self.player_var}.play();")
+
+ def pause(self):
+ ui.run_javascript(f"window.{self.player_var}.pause();")
+
+ def seek(self, time: float):
+ ui.run_javascript(f"window.{self.player_var}.currentTime = {time};")
+
+ def set_source(self, src: str, poster_url: str = "", type: str = 'video/mp4'):
+ self.src = src
+ self.poster_url = poster_url
+ ui.run_javascript(f"""
+ window.{self.player_var}.source = {{
+ type: 'video',
+ sources: [
+ {{
+ src: '{src}',
+ type: '{type}',
+ }},
+ ],
+ poster: '{poster_url}',
+ }};
+ """)
+
+ async def get_current_position(self) -> float:
+ result = await ui.run_javascript(f"return window.{self.player_var}.currentTime;")
+ return float(result)
+
+
+def install_plyr(css: str = "https://cdn.plyr.io/3.7.8/plyr.css",
+ js: str = "https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"):
+ ui.add_head_html(f'''
+
+
+
+ ''')
diff --git a/web/custom_widgets/__init__.py b/web/custom_widgets/__init__.py
index 3acf89b..1a4be00 100644
--- a/web/custom_widgets/__init__.py
+++ b/web/custom_widgets/__init__.py
@@ -1,3 +1,4 @@
+from web.custom_widgets.header import draw_header
from web.custom_widgets.content_dialog import ContentDialog
from web.custom_widgets.content_card import ContentCard
-from web.custom_widgets.header import draw_header
+from web.custom_widgets.PlyrVideoPlayer import PlyrVideoPlayer
diff --git a/web/pages/room.py b/web/pages/room.py
index 4b288a0..1a97998 100644
--- a/web/pages/room.py
+++ b/web/pages/room.py
@@ -7,6 +7,7 @@ from nicegui import ui
import config
import globals
from rooms.state import PlayerState
+from web.custom_widgets import PlyrVideoPlayer
from web.custom_widgets.header import draw_header
from web.misc import check_user
@@ -40,7 +41,7 @@ def _leave_room(room_uid: str, user_uid: str):
def _change_episode(room_uid: str, tmdb_id: int, season_number: int, episode_number: int, seasons_column: ui.column,
- video_player: ui.video, player_data: dict):
+ video_player: PlyrVideoPlayer, player_data: dict):
try:
new_episode = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id].seasons[season_number - 1].episodes[
episode_number - 1]
@@ -73,7 +74,7 @@ def _draw_users_list(room_uid: str, users_card: ui.card):
ui.label(user.username)
-def _draw_seasons(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_player: ui.video,
+def _draw_seasons(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_player: PlyrVideoPlayer,
player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
tv_show = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
@@ -95,41 +96,38 @@ def _draw_seasons(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_
episode_button.disable()
-async def _on_seeked(room_uid: str, video_player: ui.video, player_data: dict):
+async def _on_seeked(room_uid: str, video_player: PlyrVideoPlayer, player_data: dict):
try:
- position = await ui.run_javascript(f'getHtmlElement({video_player.id}).currentTime')
+ position = await video_player.get_current_position()
except TimeoutError:
return
globals.ROOMS_DATABASE.by_uid[room_uid].seek(position)
player_data["position"] = position
-async def _on_play(room_uid: str, video_player: ui.video, player_data: dict):
+async def _on_play(room_uid: str, player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.play()
player_data["state"] = PlayerState.PLAYING
- await _on_seeked(room_uid, video_player, player_data)
-async def _on_pause(room_uid: str, video_player: ui.video, player_data: dict):
+async def _on_pause(room_uid: str, player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.pause()
player_data["state"] = PlayerState.PAUSED
- await _on_seeked(room_uid, video_player, player_data)
-async def _on_stop(room_uid: str, video_player: ui.video, player_data: dict):
+async def _on_stop(room_uid: str, video_player: PlyrVideoPlayer, player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.stop()
player_data["state"] = PlayerState.STOPPED
- await _on_seeked(room_uid, video_player, player_data)
video_player.pause()
-async def _sync(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_player: ui.video,
+async def _sync(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_player: PlyrVideoPlayer,
player_data: dict):
try:
- player_data["position"] = await ui.run_javascript(f'getHtmlElement({video_player.id}).currentTime')
+ player_data["position"] = await video_player.get_current_position()
except TimeoutError:
return
room = globals.ROOMS_DATABASE.by_uid[room_uid]
@@ -146,9 +144,11 @@ async def _sync(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_pl
video_player.pause()
player_data["state"] = PlayerState.STOPPED
- if abs(player_data["position"] - room.player_position) > config.MAX_DELAY_SECONDS:
- video_player.seek(room.player_position)
- player_data["position"] = room.player_position
+ if not await video_player.is_seeking():
+ if (player_data["state"] != PlayerState.PLAYING
+ or abs(player_data["position"] - room.player_position) > config.MAX_DELAY_SECONDS):
+ video_player.seek(room.player_position)
+ player_data["position"] = room.player_position
if player_data["season"] != room.current_season or player_data["episode"] != room.current_episode:
player_data["season"], player_data["episode"] = room.current_season, room.current_episode
@@ -180,23 +180,21 @@ async def page(room_uid: str):
with ui.row(wrap=False).classes("w-full"):
player_card = ui.card()
player_card.classes("no-shadow")
- player_card.style("width: 80%; height: 85vh; border-radius: 15px")
+ player_card.style("width: 80%; min-height: 80vh; max-height: 80vh; border-radius: 15px")
player_card.style("display: flex; justify-content: center; align-items: center;")
users_card = ui.card()
users_card.classes("no-shadow")
- users_card.style("width: 20%; height: 85vh; border-radius: 15px")
+ users_card.style("width: 20%; min-height: 80vh; max-height: 80vh; border-radius: 15px")
ui.timer(1, partial(_draw_users_list, room_uid, users_card))
with player_card:
- video_player = ui.video(src="")
- video_player.classes("w-full h-full")
- video_player.style("border-radius: 15px")
+ video_player = PlyrVideoPlayer(src="")
- video_player.on("play", partial(_on_play, room_uid, video_player, player_data))
- video_player.on("pause", partial(_on_pause, room_uid, video_player, player_data))
+ video_player.on("play", partial(_on_play, room_uid, player_data))
+ video_player.on("pause", partial(_on_pause, room_uid, player_data))
video_player.on("seeked", partial(_on_seeked, room_uid, video_player, player_data))
- video_player.on("ended", partial(_on_stop, room_uid, video_player, player_data))
+ video_player.on("end", partial(_on_stop, room_uid, video_player, player_data))
seasons_column = ui.column().classes("w-full")
seasons_column.visible = False
@@ -205,9 +203,11 @@ async def page(room_uid: str):
content = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
if content.type == "movie":
- source = content.file_url
+ video = content.file_url
+ poster = content.backdrop_url
elif content.type == "tv":
- source = content.seasons[0].episodes[0].file_url
+ video = content.seasons[0].episodes[0].file_url
+ poster = ""
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.current_season, room.current_episode = 1, 1
@@ -216,9 +216,10 @@ async def page(room_uid: str):
seasons_column.visible = True
_draw_seasons(room_uid, tmdb_id, seasons_column, video_player, player_data)
else:
- source = ""
+ video = ""
+ poster = ""
- video_player.set_source(source)
+ video_player.set_source(video, poster)
ui.timer(1, partial(_sync, room_uid, tmdb_id, seasons_column, video_player, player_data))
diff --git a/web/web.py b/web/web.py
index f84b5d3..df2dadc 100644
--- a/web/web.py
+++ b/web/web.py
@@ -1,6 +1,7 @@
from nicegui import ui
import config
+from web.custom_widgets.PlyrVideoPlayer import install_plyr
from web.misc import update_user
from web.pages import *
@@ -31,10 +32,7 @@ async def rooms():
@ui.page("/room/{room_uid}")
async def room(room_uid: str):
ui.add_head_html("")
- ui.add_head_html('''
-
-
- ''')
+ install_plyr()
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
ui.timer(60, update_user)
await room_page.page(room_uid)