feat: Video player UI improvements, double click to fullscreen and click to pause

This commit is contained in:
Aleksi Lassila
2023-08-16 13:23:19 +03:00
parent 2db658bfec
commit cd99f4e593
9 changed files with 292 additions and 211 deletions

View File

@@ -108,6 +108,15 @@ PUBLIC_JELLYFIN_BASE_URL=http://127.0.0.1:8096
For Webstorm users: I'd recommend using VS Code as it has way better Svelte Typescript support. For Webstorm users: I'd recommend using VS Code as it has way better Svelte Typescript support.
Useful resources:
- https://developer.themoviedb.org/reference
- https://api.jellyfin.org/
- https://sonarr.tv/docs/api/
- https://radarr.video/docs/api/
- https://github.com/jellyfin/jellyfin-web
- Network tab in the browser in Jellyfin, Radarr & Sonarr web UIs
# Additional Screenshots # Additional Screenshots
![Landing Page](images/screenshot-1.png) ![Landing Page](images/screenshot-1.png)

View File

@@ -9,4 +9,4 @@ services:
context: . context: .
target: production target: production
ports: ports:
- 9494:3000 - 9494:9494

View File

@@ -158,7 +158,7 @@ export const getJellyfinPlaybackInfo = async (
userId, userId,
startTimeTicks, startTimeTicks,
autoOpenLiveStream: true, autoOpenLiveStream: true,
maxStreamingBitrate: maxStreamingBitrate maxStreamingBitrate
} }
}, },
body: { body: {
@@ -230,9 +230,7 @@ export const reportJellyfinPlaybackStopped = (
} }
}); });
export const delteActiveEncoding = ( export const delteActiveEncoding = (playSessionId: string) =>
playSessionId: string
) =>
JellyfinApi?.del('/Videos/ActiveEncodings', { JellyfinApi?.del('/Videos/ActiveEncodings', {
params: { params: {
query: { query: {

View File

@@ -5,60 +5,60 @@
* @param resolution The resolution of the video * @param resolution The resolution of the video
* @returns An array containing all the available qualities * @returns An array containing all the available qualities
*/ */
export function getQualities(resolution : number) { export function getQualities(resolution: number) {
// We add one to the minimum resolution since some movies // We add one to the minimum resolution since some movies
// have a resolution of 1080p, but the format isn't 16:9, // have a resolution of 1080p, but the format isn't 16:9,
// so the height is less than 1080, so we detect as 1080p // so the height is less than 1080, so we detect as 1080p
// anything higher than 720p, and so on for the other. // anything higher than 720p, and so on for the other.
let data = [ const data = [
{ {
name: "4K - 120 Mbps", name: '4K - 120 Mbps',
maxBitrate: 120000000, maxBitrate: 120000000,
minResolution: 1080 + 1 minResolution: 1080 + 1
}, },
{ {
name: "4K - 80 Mbps", name: '4K - 80 Mbps',
maxBitrate: 80000000, maxBitrate: 80000000,
minResolution: 1080 + 1 minResolution: 1080 + 1
}, },
{ {
name: "1080p - 40 Mbps", name: '1080p - 40 Mbps',
maxBitrate: 40000000, maxBitrate: 40000000,
minResolution: 720 + 1 minResolution: 720 + 1
}, },
{ {
name: "1080p - 10 Mbps", name: '1080p - 10 Mbps',
maxBitrate: 10000000, maxBitrate: 10000000,
minResolution: 720 + 1 minResolution: 720 + 1
}, },
{ {
name: "720p - 8 Mbps", name: '720p - 8 Mbps',
maxBitrate: 8000000, maxBitrate: 8000000,
minResolution: 480 + 1 minResolution: 480 + 1
}, },
{ {
name: "720p - 4 Mbps", name: '720p - 4 Mbps',
maxBitrate: 4000000, maxBitrate: 4000000,
minResolution: 480 + 1 minResolution: 480 + 1
}, },
{ {
name: "480p - 3 Mbps", name: '480p - 3 Mbps',
maxBitrate: 3000000, maxBitrate: 3000000,
minResolution: 360 + 1 minResolution: 360 + 1
}, },
{ {
name: "480p - 720 Kbps", name: '480p - 720 Kbps',
maxBitrate: 720000, maxBitrate: 720000,
minResolution: 360 + 1 minResolution: 360 + 1
}, },
{ {
name: "360p - 420 Kbps", name: '360p - 420 Kbps',
maxBitrate: 420000, maxBitrate: 420000,
minResolution: 0 minResolution: 0
} }
] ];
return data.filter((quality) => { return data.filter((quality) => {
return quality.minResolution <= resolution return quality.minResolution <= resolution;
}); });
} }

View File

@@ -74,7 +74,10 @@
style={position === 'fixed' style={position === 'fixed'
? `left: ${ ? `left: ${
fixedPosition.x - (fixedPosition.x > windowWidth / 2 ? menu?.clientWidth : 0) fixedPosition.x - (fixedPosition.x > windowWidth / 2 ? menu?.clientWidth : 0)
}px; top: ${fixedPosition.y - (bottom ? (fixedPosition.y > windowHeight / 2 ? menu?.clientHeight : 0) : 0)}px;` }px; top: ${
fixedPosition.y -
(bottom ? (fixedPosition.y > windowHeight / 2 ? menu?.clientHeight : 0) : 0)
}px;`
: menu?.getBoundingClientRect()?.left > windowWidth / 2 : menu?.getBoundingClientRect()?.left > windowWidth / 2
? `right: 0;${bottom ? 'bottom: 40px;' : ''}` ? `right: 0;${bottom ? 'bottom: 40px;' : ''}`
: `left: 0;${bottom ? 'bottom: 40px;' : ''}`} : `left: 0;${bottom ? 'bottom: 40px;' : ''}`}

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import classNames from 'classnames';
import { Check } from 'radix-icons-svelte';
import ContextMenuItem from './ContextMenuItem.svelte';
export let selected = false;
</script>
<ContextMenuItem on:click>
<div class="flex items-center gap-2 justify-between cursor-pointer">
<Check
size={20}
class={classNames({
'opacity-0': !selected,
'opacity-100': selected
})}
/>
<div class="flex items-center text-left w-32">
<slot />
</div>
</div>
</ContextMenuItem>

View File

@@ -1,19 +0,0 @@
<script lang="ts">
import classNames from 'classnames';
import { Check } from "radix-icons-svelte";
import ContextMenuItem from "./ContextMenuItem.svelte";
export let selected = false;
</script>
<ContextMenuItem on:click>
<div class="flex items-center justify-between cursor-pointer">
<Check size={20} class={classNames('mr-4', {
'opacity-0': !selected,
'opacity-100': selected
})} />
<div class="flex items-center text-left w-32">
<slot />
</div>
</div>
</ContextMenuItem>

View File

@@ -1,39 +1,47 @@
<script lang="ts"> <script lang="ts">
export let min = 0; export let min = 0;
export let max = 100; export let max = 100;
export let step = 0.01; export let step = 0.01;
export let primaryValue = 0; export let primaryValue = 0;
export let secondaryValue = 0; export let secondaryValue = 0;
let progressBarOffset = 0; let progressBarOffset = 0;
</script> </script>
<div class="h-2 relative group"> <div class="h-1 relative group">
<!-- 0.54 em is half the width of the input thumb size --> <div class="h-full relative px-[0.5rem]">
<div class="h-full relative px-[0.54em]"> <div class="h-full bg-zinc-200 bg-opacity-20 rounded-full overflow-hidden relative">
<div class="h-full bg-gray-300 rounded-full overflow-hidden relative"> <!-- Secondary progress -->
<!-- Secondary progress --> <div
<div class="h-full bg-gray-400 absolute top-0" class="h-full bg-zinc-200 bg-opacity-20 absolute top-0"
style="width: {secondaryValue / max * 100}%;"> style="width: {(secondaryValue / max) * 100}%;"
</div> />
<!-- Primary progress --> <!-- Primary progress -->
<div class="h-full bg-amber-200 absolute top-0" <div
style="width: {primaryValue / max * 100}%;" class="h-full bg-amber-300 absolute top-0"
bind:offsetWidth={progressBarOffset}> style="width: {(primaryValue / max) * 100}%;"
</div> bind:offsetWidth={progressBarOffset}
</div> />
</div>
<div class="absolute w-4 h-4 bg-amber-200 rounded-full transform mx-2 -translate-x-1/2 -translate-y-1/2 top-1/2 cursor-pointer <div
drop-shadow-md group-hover:scale-125 group-hover:shadow-lg transition-transform duration-100" class="absolute w-3 h-3 bg-amber-200 rounded-full transform mx-2 -translate-x-1/2 -translate-y-1/2 top-1/2 cursor-pointer
style="left: {progressBarOffset}px;" drop-shadow-md opacity-0 group-hover:opacity-100 transition-opacity duration-100"
></div> style="left: {progressBarOffset}px;"
</div> />
</div>
<input <input
type="range" type="range"
class="w-full h-full absolute cursor-pointer opacity-0 transform -translate-y-2" class="w-full absolute -top-0.5 cursor-pointer h-2 opacity-0"
min={min} max={max} step={step} bind:value={primaryValue} on:mouseup on:mousedown {min}
on:touchstart on:touchend {max}
/> {step}
bind:value={primaryValue}
on:mouseup
on:mousedown
on:touchstart
on:touchend
/>
</div> </div>

View File

@@ -6,23 +6,33 @@
reportJellyfinPlaybackProgress, reportJellyfinPlaybackProgress,
reportJellyfinPlaybackStarted, reportJellyfinPlaybackStarted,
reportJellyfinPlaybackStopped, reportJellyfinPlaybackStopped,
delteActiveEncoding delteActiveEncoding as deleteActiveEncoding
} from '$lib/apis/jellyfin/jellyfinApi'; } from '$lib/apis/jellyfin/jellyfinApi';
import { getQualities } from '$lib/apis/jellyfin/qualities'; import { getQualities } from '$lib/apis/jellyfin/qualities';
import getDeviceProfile from '$lib/apis/jellyfin/playback-profiles'; import getDeviceProfile from '$lib/apis/jellyfin/playback-profiles';
import classNames from 'classnames'; import classNames from 'classnames';
import Hls from 'hls.js'; import Hls from 'hls.js';
import { Cross2, Play, Pause, EnterFullScreen, ExitFullScreen, import {
SpeakerLoud, SpeakerModerate, SpeakerQuiet, SpeakerOff, Gear } from 'radix-icons-svelte'; Cross2,
Play,
Pause,
EnterFullScreen,
ExitFullScreen,
SpeakerLoud,
SpeakerModerate,
SpeakerQuiet,
SpeakerOff,
Gear
} from 'radix-icons-svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import IconButton from '../IconButton.svelte'; import IconButton from '../IconButton.svelte';
import { playerState } from './VideoPlayer'; import { playerState } from './VideoPlayer';
import { modalStack } from '../Modal/Modal'; import { modalStack } from '../Modal/Modal';
import { JELLYFIN_BASE_URL } from '$lib/constants'; import { JELLYFIN_BASE_URL } from '$lib/constants';
import { contextMenu } from '../ContextMenu/ContextMenu'; import { contextMenu } from '../ContextMenu/ContextMenu';
import Slider from './Slider.svelte' import Slider from './Slider.svelte';
import ContextMenu from '../ContextMenu/ContextMenu.svelte'; import ContextMenu from '../ContextMenu/ContextMenu.svelte';
import SelectableMenuItem from '../ContextMenu/SelectableMenuItem.svelte'; import SelectableContextMenuItem from '../ContextMenu/SelectableContextMenuItem.svelte';
export let modalId: symbol; export let modalId: symbol;
@@ -37,7 +47,7 @@
let progressInterval: NodeJS.Timeout; let progressInterval: NodeJS.Timeout;
// These functions are different in every browser // These functions are different in every browser
let reqFullscreenFunc: ((elem : HTMLElement) => void) | undefined = undefined; let reqFullscreenFunc: ((elem: HTMLElement) => void) | undefined = undefined;
let exitFullscreen: (() => void) | undefined = undefined; let exitFullscreen: (() => void) | undefined = undefined;
let fullscreenChangeEvent: string | undefined = undefined; let fullscreenChangeEvent: string | undefined = undefined;
let getFullscreenElement: (() => HTMLElement) | undefined = undefined; let getFullscreenElement: (() => HTMLElement) | undefined = undefined;
@@ -46,60 +56,72 @@
let elem = document.createElement('div'); let elem = document.createElement('div');
// @ts-ignore // @ts-ignore
if (elem.requestFullscreen) { if (elem.requestFullscreen) {
reqFullscreenFunc = (elem) => { elem.requestFullscreen(); }; reqFullscreenFunc = (elem) => {
elem.requestFullscreen();
};
fullscreenChangeEvent = 'fullscreenchange'; fullscreenChangeEvent = 'fullscreenchange';
getFullscreenElement = () => <HTMLElement> document.fullscreenElement; getFullscreenElement = () => <HTMLElement>document.fullscreenElement;
if (document.exitFullscreen) exitFullscreen = () => document.exitFullscreen(); if (document.exitFullscreen) exitFullscreen = () => document.exitFullscreen();
// @ts-ignore
} else if (elem.webkitRequestFullscreen) {
// @ts-ignore // @ts-ignore
reqFullscreenFunc = (elem) => { elem.webkitRequestFullscreen(); }; } else if (elem.webkitRequestFullscreen) {
reqFullscreenFunc = (elem) => {
// @ts-ignore
elem.webkitRequestFullscreen();
};
fullscreenChangeEvent = 'webkitfullscreenchange'; fullscreenChangeEvent = 'webkitfullscreenchange';
// @ts-ignore // @ts-ignore
getFullscreenElement = () => <HTMLElement> document.webkitFullscreenElement; getFullscreenElement = () => <HTMLElement>document.webkitFullscreenElement;
// @ts-ignore
if (document.webkitExitFullscreen) exitFullscreen = () => document.webkitExitFullscreen();
// @ts-ignore
} else if (elem.msRequestFullscreen) {
// @ts-ignore // @ts-ignore
reqFullscreenFunc = (elem) => { elem.msRequestFullscreen(); } if (document.webkitExitFullscreen) exitFullscreen = () => document.webkitExitFullscreen();
// @ts-ignore
} else if (elem.msRequestFullscreen) {
reqFullscreenFunc = (elem) => {
// @ts-ignore
elem.msRequestFullscreen();
};
fullscreenChangeEvent = 'MSFullscreenChange'; fullscreenChangeEvent = 'MSFullscreenChange';
// @ts-ignore // @ts-ignore
getFullscreenElement = () => <HTMLElement> document.msFullscreenElement; getFullscreenElement = () => <HTMLElement>document.msFullscreenElement;
// @ts-ignore // @ts-ignore
if (document.msExitFullscreen) exitFullscreen = () => document.msExitFullscreen(); if (document.msExitFullscreen) exitFullscreen = () => document.msExitFullscreen();
// @ts-ignore
} else if (elem.mozRequestFullScreen) {
// @ts-ignore // @ts-ignore
reqFullscreenFunc = (elem) => { elem.mozRequestFullScreen(); }; } else if (elem.mozRequestFullScreen) {
reqFullscreenFunc = (elem) => {
// @ts-ignore
elem.mozRequestFullScreen();
};
fullscreenChangeEvent = 'mozfullscreenchange'; fullscreenChangeEvent = 'mozfullscreenchange';
// @ts-ignore // @ts-ignore
getFullscreenElement = () => <HTMLElement> document.mozFullScreenElement; getFullscreenElement = () => <HTMLElement>document.mozFullScreenElement;
// @ts-ignore // @ts-ignore
if (document.mozCancelFullScreen) exitFullscreen = () => document.mozCancelFullScreen(); if (document.mozCancelFullScreen) exitFullscreen = () => document.mozCancelFullScreen();
} }
let paused : boolean; let paused: boolean;
let duration : number = 0; let duration: number = 0;
let displayedTime : number = 0; let displayedTime: number = 0;
let bufferedTime : number = 0; let bufferedTime: number = 0;
let videoLoaded : boolean = false; let videoLoaded: boolean = false;
let seeking : boolean = false; let seeking: boolean = false;
let playerStateBeforeSeek : boolean; let playerStateBeforeSeek: boolean;
let fullscreen : boolean = false; let fullscreen: boolean = false;
let volume : number = 1; let volume: number = 1;
let mute : boolean = false; let mute: boolean = false;
let resolution : number = 1080; let resolution: number = 1080;
let currentBitrate : number = 0; let currentBitrate: number = 0;
let shouldCloseUi = false; let shouldCloseUi = false;
let uiVisible = true; let uiVisible = true;
$: uiVisible = !shouldCloseUi || seeking || paused || $contextMenu === qualityContextMenuId $: uiVisible = !shouldCloseUi || seeking || paused || $contextMenu === qualityContextMenuId;
const fetchPlaybackInfo = (itemId: string, maxBitrate: number | undefined = undefined, starting : boolean = true) => const fetchPlaybackInfo = (
itemId: string,
maxBitrate: number | undefined = undefined,
starting: boolean = true
) =>
getJellyfinItem(itemId).then((item) => getJellyfinItem(itemId).then((item) =>
getJellyfinPlaybackInfo( getJellyfinPlaybackInfo(
itemId, itemId,
@@ -128,10 +150,10 @@
hls.attachMedia(video); hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
/* /*
* HLS.js does NOT work on iOS on iPhone because Safari on iPhone does not support MSE. * HLS.js does NOT work on iOS on iPhone because Safari on iPhone does not support MSE.
* This is not a problem, since HLS is natively supported on iOS. But any other browser * This is not a problem, since HLS is natively supported on iOS. But any other browser
* that does not support MSE will not be able to play the video. * that does not support MSE will not be able to play the video.
*/ */
video.src = JELLYFIN_BASE_URL + playbackUri; video.src = JELLYFIN_BASE_URL + playbackUri;
} else { } else {
throw new Error('HLS is not supported'); throw new Error('HLS is not supported');
@@ -154,7 +176,8 @@
// A start report should only be sent when the video starts playing, // A start report should only be sent when the video starts playing,
// not every time a playback info request is made // not every time a playback info request is made
if (mediaSourceId && starting) await reportJellyfinPlaybackStarted(itemId, sessionId, mediaSourceId); if (mediaSourceId && starting)
await reportJellyfinPlaybackStarted(itemId, sessionId, mediaSourceId);
reportProgress = async () => { reportProgress = async () => {
await reportJellyfinPlaybackProgress( await reportJellyfinPlaybackProgress(
@@ -163,7 +186,7 @@
video?.paused == true, video?.paused == true,
video?.currentTime * 10_000_000 video?.currentTime * 10_000_000
); );
} };
if (progressInterval) clearInterval(progressInterval); if (progressInterval) clearInterval(progressInterval);
progressInterval = setInterval(() => { progressInterval = setInterval(() => {
@@ -172,8 +195,8 @@
}, 5000); }, 5000);
deleteEncoding = () => { deleteEncoding = () => {
delteActiveEncoding(sessionId); deleteActiveEncoding(sessionId);
} };
stopCallback = () => { stopCallback = () => {
reportJellyfinPlaybackStopped(itemId, sessionId, video?.currentTime * 10_000_000); reportJellyfinPlaybackStopped(itemId, sessionId, video?.currentTime * 10_000_000);
@@ -220,7 +243,7 @@
modalStack.close(modalId); modalStack.close(modalId);
} }
function handleUserInteraction(touch : boolean = false) { function handleUserInteraction(touch: boolean = false) {
if (touch) shouldCloseUi = !shouldCloseUi; if (touch) shouldCloseUi = !shouldCloseUi;
else shouldCloseUi = false; else shouldCloseUi = false;
@@ -239,7 +262,7 @@
else contextMenu.show(qualityContextMenuId); else contextMenu.show(qualityContextMenuId);
} }
async function handleSelectQuality(bitrate : number) { async function handleSelectQuality(bitrate: number) {
if (!$playerState.jellyfinId || !video || seeking) return; if (!$playerState.jellyfinId || !video || seeking) return;
if (bitrate === currentBitrate) return; if (bitrate === currentBitrate) return;
@@ -254,7 +277,7 @@
paused = stateBeforeLoad; paused = stateBeforeLoad;
} }
function secondsToTime(seconds : number, forceHours = false) { function secondsToTime(seconds: number, forceHours = false) {
if (isNaN(seconds)) return '00:00'; if (isNaN(seconds)) return '00:00';
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
@@ -307,78 +330,115 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class={classNames("bg-black w-screen h-screen relative flex items-center justify-center", { class={classNames(
'cursor-none': !uiVisible 'bg-black w-screen h-[100dvh] sm:h-screen relative flex items-center justify-center',
})} {
'cursor-none': !uiVisible
}
)}
> >
<div class="bg-black w-screen h-screen flex items-center justify-center" bind:this={videoWrapper} <!-- svelte-ignore a11y-click-events-have-key-events -->
on:mousemove={() => handleUserInteraction(false)} on:touchend|preventDefault={() => handleUserInteraction(true)}> <div
class="bg-black w-screen h-screen flex items-center justify-center"
bind:this={videoWrapper}
on:mousemove={() => handleUserInteraction(false)}
on:touchend|preventDefault={() => handleUserInteraction(true)}
on:dblclick|preventDefault={() => (fullscreen = !fullscreen)}
on:click={() => (paused = !paused)}
>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video bind:this={video} bind:paused bind:duration on:timeupdate={() => displayedTime = (!seeking && videoLoaded) ? video.currentTime : displayedTime} <video
on:progress={() => handleBuffer()} on:play={() => { bind:this={video}
bind:paused
bind:duration
on:timeupdate={() =>
(displayedTime = !seeking && videoLoaded ? video.currentTime : displayedTime)}
on:progress={() => handleBuffer()}
on:play={() => {
if (seeking) video?.pause(); if (seeking) video?.pause();
}} }}
on:loadeddata={() => { on:loadeddata={() => {
video.currentTime = displayedTime; video.currentTime = displayedTime;
videoLoaded = true; videoLoaded = true;
}} }}
bind:volume bind:muted={mute} class="sm:w-full sm:h-full" playsinline={true} /> bind:volume
bind:muted={mute}
class="sm:w-full sm:h-full"
playsinline={true}
/>
{#if uiVisible} {#if uiVisible}
<!-- Video controls --> <!-- Video controls -->
<div class="absolute bottom-0 w-screen bg-gradient-to-t from-black/[.8] via-60% via-black-opacity-80 to-transparent" <div
on:touchend|stopPropagation transition:fade={{duration: 100}}> class="absolute bottom-0 w-screen bg-gradient-to-t from-black/[.8] via-60% via-black-opacity-80 to-transparent"
<div class="flex flex-col items-center space-y-4 p-5 w-full"> on:touchend|stopPropagation
<div class = "flex items-center space-x-4 w-full"> transition:fade={{ duration: 100 }}
<span class="whitespace-nowrap tabular-nums">{secondsToTime(displayedTime, duration > 3600)}</span> >
<div class="flex flex-col items-center p-4 gap-2 w-full">
<div class="flex items-center text-sm w-full">
<span class="whitespace-nowrap tabular-nums"
>{secondsToTime(displayedTime, duration > 3600)}</span
>
<div class="flex-grow"> <div class="flex-grow">
<Slider bind:primaryValue={displayedTime} <Slider
secondaryValue={bufferedTime} bind:primaryValue={displayedTime}
max={duration} secondaryValue={bufferedTime}
on:mousedown={onSeekStart} max={duration}
on:mouseup={onSeekEnd} on:mousedown={onSeekStart}
on:touchstart={onSeekStart} on:mouseup={onSeekEnd}
on:touchend={onSeekEnd}/> on:touchstart={onSeekStart}
on:touchend={onSeekEnd}
/>
</div> </div>
<span class="whitespace-nowrap tabular-nums">{secondsToTime(duration)}</span> <span class="whitespace-nowrap tabular-nums">{secondsToTime(duration)}</span>
</div> </div>
<div class="flex items-center justify-between mb-2 w-full"> <div class="flex items-center justify-between mb-2 w-full">
<IconButton on:click={() => paused = !paused}> <IconButton on:click={() => (paused = !paused)}>
{#if (!seeking && paused) || (seeking && playerStateBeforeSeek)} {#if (!seeking && paused) || (seeking && playerStateBeforeSeek)}
<Play size={25} /> <Play size={20} />
{:else} {:else}
<Pause size={25} /> <Pause size={20} />
{/if} {/if}
</IconButton> </IconButton>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="relative"> <div class="relative">
<ContextMenu heading="Quality" position="absolute" bottom={true} id={qualityContextMenuId}> <ContextMenu
heading="Quality"
position="absolute"
bottom={true}
id={qualityContextMenuId}
>
<svelte:fragment slot="menu"> <svelte:fragment slot="menu">
{#each getQualities(resolution) as quality} {#each getQualities(resolution) as quality}
<SelectableMenuItem <SelectableContextMenuItem
selected={quality.maxBitrate === currentBitrate} selected={quality.maxBitrate === currentBitrate}
on:click={() => handleSelectQuality(quality.maxBitrate)}> on:click={() => handleSelectQuality(quality.maxBitrate)}
>
{quality.name} {quality.name}
</SelectableMenuItem> </SelectableContextMenuItem>
{/each} {/each}
</svelte:fragment> </svelte:fragment>
<IconButton on:click={handleQualityToggleVisibility}> <IconButton on:click={handleQualityToggleVisibility}>
<Gear size={25} /> <Gear size={20} />
</IconButton> </IconButton>
</ContextMenu> </ContextMenu>
</div> </div>
<IconButton on:click={() => {mute = !mute;}}> <IconButton
on:click={() => {
mute = !mute;
}}
>
{#if volume == 0 || mute} {#if volume == 0 || mute}
<SpeakerOff size={25} /> <SpeakerOff size={20} />
{:else if volume < 0.25} {:else if volume < 0.25}
<SpeakerQuiet size={25} /> <SpeakerQuiet size={20} />
{:else if volume < 0.90} {:else if volume < 0.9}
<SpeakerModerate size={25} /> <SpeakerModerate size={20} />
{:else} {:else}
<SpeakerLoud size={25} /> <SpeakerLoud size={20} />
{/if} {/if}
</IconButton> </IconButton>
@@ -387,17 +447,17 @@
</div> </div>
{#if reqFullscreenFunc} {#if reqFullscreenFunc}
<IconButton on:click={() => fullscreen = !fullscreen}> <IconButton on:click={() => (fullscreen = !fullscreen)}>
{#if fullscreen} {#if fullscreen}
<ExitFullScreen size={25} /> <ExitFullScreen size={20} />
{:else if !fullscreen && exitFullscreen} {:else if !fullscreen && exitFullscreen}
<EnterFullScreen size={25} /> <EnterFullScreen size={20} />
{/if} {/if}
</IconButton> </IconButton>
<!-- Edge case to allow fullscreen on iPhone --> <!-- Edge case to allow fullscreen on iPhone -->
{:else if video?.webkitEnterFullScreen} {:else if video?.webkitEnterFullScreen}
<IconButton on:click={() => video.webkitEnterFullScreen()}> <IconButton on:click={() => video.webkitEnterFullScreen()}>
<EnterFullScreen size={25} /> <EnterFullScreen size={20} />
</IconButton> </IconButton>
{/if} {/if}
</div> </div>
@@ -408,7 +468,7 @@
</div> </div>
{#if uiVisible} {#if uiVisible}
<div class='absolute top-4 right-8 z-50' transition:fade={{duration: 100}}> <div class="absolute top-4 right-8 z-50" transition:fade={{ duration: 100 }}>
<IconButton on:click={handleClose}> <IconButton on:click={handleClose}>
<Cross2 size={25} /> <Cross2 size={25} />
</IconButton> </IconButton>