replace ui.video with Plyr

This commit is contained in:
2025-07-21 04:40:16 +03:00
parent 825fb49f27
commit 5318cc4680
4 changed files with 150 additions and 32 deletions
+118
View File
@@ -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"<source src=\"{src}\" type=\"video/mp4\" />"
video_html = (
f"<video id=\"{self.element_id}\" playsinline controls {poster_attr}>"
f"{source_html}"
"</video>"
)
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'''
<link rel="stylesheet" href="{css}" />
<script src="{js}"></script>
<style>
.plyr-container {{
border-radius: 15px;
overflow: hidden;
width: 100%;
height: 100%;
}}
</style>
''')
+2 -1
View File
@@ -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
+28 -27
View File
@@ -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))
+2 -4
View File
@@ -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("<meta name=\"referrer\" content=\"no-referrer\" />")
ui.add_head_html('''
<link href="https://vjs.zencdn.net/7.21.1/video-js.css" rel="stylesheet" />
<script src="https://vjs.zencdn.net/7.21.1/video.min.js"></script>
''')
install_plyr()
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
ui.timer(60, update_user)
await room_page.page(room_uid)