replace ui.video with Plyr
This commit is contained in:
@@ -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>
|
||||
''')
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user