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_dialog import ContentDialog
|
||||||
from web.custom_widgets.content_card import ContentCard
|
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 config
|
||||||
import globals
|
import globals
|
||||||
from rooms.state import PlayerState
|
from rooms.state import PlayerState
|
||||||
|
from web.custom_widgets import PlyrVideoPlayer
|
||||||
from web.custom_widgets.header import draw_header
|
from web.custom_widgets.header import draw_header
|
||||||
from web.misc import check_user
|
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,
|
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:
|
try:
|
||||||
new_episode = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id].seasons[season_number - 1].episodes[
|
new_episode = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id].seasons[season_number - 1].episodes[
|
||||||
episode_number - 1]
|
episode_number - 1]
|
||||||
@@ -73,7 +74,7 @@ def _draw_users_list(room_uid: str, users_card: ui.card):
|
|||||||
ui.label(user.username)
|
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):
|
player_data: dict):
|
||||||
room = globals.ROOMS_DATABASE.by_uid[room_uid]
|
room = globals.ROOMS_DATABASE.by_uid[room_uid]
|
||||||
tv_show = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
|
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()
|
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:
|
try:
|
||||||
position = await ui.run_javascript(f'getHtmlElement({video_player.id}).currentTime')
|
position = await video_player.get_current_position()
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
return
|
return
|
||||||
globals.ROOMS_DATABASE.by_uid[room_uid].seek(position)
|
globals.ROOMS_DATABASE.by_uid[room_uid].seek(position)
|
||||||
player_data["position"] = 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 = globals.ROOMS_DATABASE.by_uid[room_uid]
|
||||||
room.play()
|
room.play()
|
||||||
player_data["state"] = PlayerState.PLAYING
|
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 = globals.ROOMS_DATABASE.by_uid[room_uid]
|
||||||
room.pause()
|
room.pause()
|
||||||
player_data["state"] = PlayerState.PAUSED
|
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 = globals.ROOMS_DATABASE.by_uid[room_uid]
|
||||||
room.stop()
|
room.stop()
|
||||||
player_data["state"] = PlayerState.STOPPED
|
player_data["state"] = PlayerState.STOPPED
|
||||||
await _on_seeked(room_uid, video_player, player_data)
|
|
||||||
video_player.pause()
|
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):
|
player_data: dict):
|
||||||
try:
|
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:
|
except TimeoutError:
|
||||||
return
|
return
|
||||||
room = globals.ROOMS_DATABASE.by_uid[room_uid]
|
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()
|
video_player.pause()
|
||||||
player_data["state"] = PlayerState.STOPPED
|
player_data["state"] = PlayerState.STOPPED
|
||||||
|
|
||||||
if abs(player_data["position"] - room.player_position) > config.MAX_DELAY_SECONDS:
|
if not await video_player.is_seeking():
|
||||||
video_player.seek(room.player_position)
|
if (player_data["state"] != PlayerState.PLAYING
|
||||||
player_data["position"] = room.player_position
|
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:
|
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
|
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"):
|
with ui.row(wrap=False).classes("w-full"):
|
||||||
player_card = ui.card()
|
player_card = ui.card()
|
||||||
player_card.classes("no-shadow")
|
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;")
|
player_card.style("display: flex; justify-content: center; align-items: center;")
|
||||||
|
|
||||||
users_card = ui.card()
|
users_card = ui.card()
|
||||||
users_card.classes("no-shadow")
|
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))
|
ui.timer(1, partial(_draw_users_list, room_uid, users_card))
|
||||||
|
|
||||||
with player_card:
|
with player_card:
|
||||||
video_player = ui.video(src="")
|
video_player = PlyrVideoPlayer(src="")
|
||||||
video_player.classes("w-full h-full")
|
|
||||||
video_player.style("border-radius: 15px")
|
|
||||||
|
|
||||||
video_player.on("play", partial(_on_play, 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, video_player, 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("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 = ui.column().classes("w-full")
|
||||||
seasons_column.visible = False
|
seasons_column.visible = False
|
||||||
@@ -205,9 +203,11 @@ async def page(room_uid: str):
|
|||||||
content = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
|
content = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
|
||||||
|
|
||||||
if content.type == "movie":
|
if content.type == "movie":
|
||||||
source = content.file_url
|
video = content.file_url
|
||||||
|
poster = content.backdrop_url
|
||||||
elif content.type == "tv":
|
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 = globals.ROOMS_DATABASE.by_uid[room_uid]
|
||||||
room.current_season, room.current_episode = 1, 1
|
room.current_season, room.current_episode = 1, 1
|
||||||
@@ -216,9 +216,10 @@ async def page(room_uid: str):
|
|||||||
seasons_column.visible = True
|
seasons_column.visible = True
|
||||||
_draw_seasons(room_uid, tmdb_id, seasons_column, video_player, player_data)
|
_draw_seasons(room_uid, tmdb_id, seasons_column, video_player, player_data)
|
||||||
else:
|
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))
|
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
|
from nicegui import ui
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
from web.custom_widgets.PlyrVideoPlayer import install_plyr
|
||||||
from web.misc import update_user
|
from web.misc import update_user
|
||||||
from web.pages import *
|
from web.pages import *
|
||||||
|
|
||||||
@@ -31,10 +32,7 @@ async def rooms():
|
|||||||
@ui.page("/room/{room_uid}")
|
@ui.page("/room/{room_uid}")
|
||||||
async def room(room_uid: str):
|
async def room(room_uid: str):
|
||||||
ui.add_head_html("<meta name=\"referrer\" content=\"no-referrer\" />")
|
ui.add_head_html("<meta name=\"referrer\" content=\"no-referrer\" />")
|
||||||
ui.add_head_html('''
|
install_plyr()
|
||||||
<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>
|
|
||||||
''')
|
|
||||||
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
|
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
|
||||||
ui.timer(60, update_user)
|
ui.timer(60, update_user)
|
||||||
await room_page.page(room_uid)
|
await room_page.page(room_uid)
|
||||||
|
|||||||
Reference in New Issue
Block a user