Merge branch 'dev' of https://github.com/movie-web/movie-web into subtitle-file-type-control

This commit is contained in:
frost768 2023-04-17 17:49:31 +03:00
commit a910c1c18c
20 changed files with 222 additions and 57 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "movie-web", "name": "movie-web",
"version": "3.0.9", "version": "3.0.10",
"private": true, "private": true,
"homepage": "https://movie.squeezebox.dev", "homepage": "https://movie.squeezebox.dev",
"dependencies": { "dependencies": {

View File

@ -22,6 +22,7 @@ registerProvider({
displayName: "NetFilm", displayName: "NetFilm",
rank: 15, rank: 15,
type: [MWMediaType.MOVIE, MWMediaType.SERIES], type: [MWMediaType.MOVIE, MWMediaType.SERIES],
disabled: true, // The creator has asked us (very nicely) to leave him alone. Until (if) we self-host, netfilm should remain disabled
async scrape({ media, episode, progress }) { async scrape({ media, episode, progress }) {
if (!this.type.includes(media.meta.type)) { if (!this.type.includes(media.meta.type)) {

View File

@ -40,6 +40,7 @@ export enum Icons {
WATCH_PARTY = "watch_party", WATCH_PARTY = "watch_party",
PICTURE_IN_PICTURE = "pictureInPicture", PICTURE_IN_PICTURE = "pictureInPicture",
CHECKMARK = "checkmark", CHECKMARK = "checkmark",
TACHOMETER = "tachometer",
} }
export interface IconProps { export interface IconProps {
@ -87,6 +88,7 @@ const iconList: Record<Icons, string> = {
watch_party: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M319.4 372c48.5-31.3 80.6-85.9 80.6-148c0-97.2-78.8-176-176-176S48 126.8 48 224c0 62.1 32.1 116.6 80.6 148c1.2 17.3 4 38 7.2 57.1l.2 1C56 395.8 0 316.5 0 224C0 100.3 100.3 0 224 0S448 100.3 448 224c0 92.5-56 171.9-136 206.1l.2-1.1c3.1-19.2 6-39.8 7.2-57zm-2.3-38.1c-1.6-5.7-3.9-11.1-7-16.2c-5.8-9.7-13.5-17-21.9-22.4c19.5-17.6 31.8-43 31.8-71.3c0-53-43-96-96-96s-96 43-96 96c0 28.3 12.3 53.8 31.8 71.3c-8.4 5.4-16.1 12.7-21.9 22.4c-3.1 5.1-5.4 10.5-7 16.2C99.8 307.5 80 268 80 224c0-79.5 64.5-144 144-144s144 64.5 144 144c0 44-19.8 83.5-50.9 109.9zM224 312c32.9 0 64 8.6 64 43.8c0 33-12.9 104.1-20.6 132.9c-5.1 19-24.5 23.4-43.4 23.4s-38.2-4.4-43.4-23.4c-7.8-28.5-20.6-99.7-20.6-132.8c0-35.1 31.1-43.8 64-43.8zm0-144a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>`, watch_party: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M319.4 372c48.5-31.3 80.6-85.9 80.6-148c0-97.2-78.8-176-176-176S48 126.8 48 224c0 62.1 32.1 116.6 80.6 148c1.2 17.3 4 38 7.2 57.1l.2 1C56 395.8 0 316.5 0 224C0 100.3 100.3 0 224 0S448 100.3 448 224c0 92.5-56 171.9-136 206.1l.2-1.1c3.1-19.2 6-39.8 7.2-57zm-2.3-38.1c-1.6-5.7-3.9-11.1-7-16.2c-5.8-9.7-13.5-17-21.9-22.4c19.5-17.6 31.8-43 31.8-71.3c0-53-43-96-96-96s-96 43-96 96c0 28.3 12.3 53.8 31.8 71.3c-8.4 5.4-16.1 12.7-21.9 22.4c-3.1 5.1-5.4 10.5-7 16.2C99.8 307.5 80 268 80 224c0-79.5 64.5-144 144-144s144 64.5 144 144c0 44-19.8 83.5-50.9 109.9zM224 312c32.9 0 64 8.6 64 43.8c0 33-12.9 104.1-20.6 132.9c-5.1 19-24.5 23.4-43.4 23.4s-38.2-4.4-43.4-23.4c-7.8-28.5-20.6-99.7-20.6-132.8c0-35.1 31.1-43.8 64-43.8zm0-144a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>`,
pictureInPicture: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/></svg>`, pictureInPicture: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/></svg>`,
checkmark: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M9 22l-10-10.598 2.798-2.859 7.149 7.473 13.144-14.016 2.909 2.806z" /></svg>`, checkmark: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M9 22l-10-10.598 2.798-2.859 7.149 7.473 13.144-14.016 2.909 2.806z" /></svg>`,
tachometer: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 576 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M128 288c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32zm154.65-97.08l16.24-48.71c1.16-3.45 3.18-6.35 4.92-9.43-4.73-2.76-9.94-4.78-15.81-4.78-17.67 0-32 14.33-32 32 0 15.78 11.63 28.29 26.65 30.92zM176 176c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32zM288 32C128.94 32 0 160.94 0 320c0 52.8 14.25 102.26 39.06 144.8 5.61 9.62 16.3 15.2 27.44 15.2h443c11.14 0 21.83-5.58 27.44-15.2C561.75 422.26 576 372.8 576 320c0-159.06-128.94-288-288-288zm212.27 400H75.73C57.56 397.63 48 359.12 48 320 48 187.66 155.66 80 288 80s240 107.66 240 240c0 39.12-9.56 77.63-27.73 112zM416 320c0 17.67 14.33 32 32 32s32-14.33 32-32-14.33-32-32-32-32 14.33-32 32zm-56.41-182.77c-12.72-4.23-26.16 2.62-30.38 15.17l-45.34 136.01C250.49 290.58 224 318.06 224 352c0 11.72 3.38 22.55 8.88 32h110.25c5.5-9.45 8.88-20.28 8.88-32 0-19.45-8.86-36.66-22.55-48.4l45.34-136.01c4.17-12.57-2.64-26.17-15.21-30.36zM432 208c0-15.8-11.66-28.33-26.72-30.93-.07.21-.07.43-.14.65l-19.5 58.49c4.37 2.24 9.11 3.8 14.36 3.8 17.67-.01 32-14.34 32-32.01z"/></svg>`,
}; };
function ChromeCastButton() { function ChromeCastButton() {

47
src/components/Slider.tsx Normal file
View File

@ -0,0 +1,47 @@
import { ChangeEventHandler, useEffect, useRef } from "react";
export type SliderProps = {
label?: string;
min: number;
max: number;
step: number;
value?: number;
valueDisplay?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
};
export function Slider(props: SliderProps) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
const e = ref.current as HTMLInputElement;
e.style.setProperty("--value", e.value);
e.style.setProperty("--min", e.min === "" ? "0" : e.min);
e.style.setProperty("--max", e.max === "" ? "100" : e.max);
e.addEventListener("input", () => e.style.setProperty("--value", e.value));
}, [ref]);
return (
<div className="mb-6 flex flex-row gap-4">
<div className="flex w-full flex-col gap-2">
{props.label ? (
<label className="font-bold">{props.label}</label>
) : null}
<input
type="range"
ref={ref}
className="styled-slider slider-progress mt-[20px]"
onChange={props.onChange}
value={props.value}
max={props.max}
min={props.min}
step={props.step}
/>
</div>
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
<div className="text-center font-bold text-white">
{props.valueDisplay ?? props.value}
</div>
</div>
</div>
);
}

View File

@ -29,6 +29,7 @@ export function FloatingView(props: Props) {
data-floating-page={props.show ? "true" : undefined} data-floating-page={props.show ? "true" : undefined}
style={{ style={{
height: props.height ? `${props.height}px` : undefined, height: props.height ? `${props.height}px` : undefined,
maxHeight: "70vh",
width: props.width ? width : undefined, width: props.width ? width : undefined,
}} }}
> >

View File

@ -21,8 +21,20 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
})); }));
const bind = useDrag( const bind = useDrag(
({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => { ({
last,
velocity: [, vy],
direction: [, dy],
movement: [, my],
...event
}) => {
if (closing.current) return; if (closing.current) return;
const isInScrollable = (event.target as HTMLDivElement).closest(
".overflow-y-auto"
);
if (isInScrollable) return; // Don't attempt to swipe the thing away if it's a scroll area unless the scroll area is at the top and the user is swiping down
const height = cardRect?.height ?? 0; const height = cardRect?.height ?? 0;
if (last) { if (last) {
// if past half height downwards // if past half height downwards
@ -69,7 +81,7 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
return ( return (
<div <div
className="absolute inset-x-0 mx-auto max-w-[400px] origin-bottom-left touch-none" className="is-mobile-view absolute inset-x-0 mx-auto max-w-[400px] origin-bottom-left touch-none"
style={{ style={{
transform: `translateY(${ transform: `translateY(${
window.innerHeight - (cardRect?.height ?? 0) + 200 window.innerHeight - (cardRect?.height ?? 0) + 200

View File

@ -38,6 +38,7 @@ body[data-no-select] {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
@ -55,6 +56,10 @@ body[data-no-select] {
@apply brightness-[500]; @apply brightness-[500];
} }
.is-mobile-view .overflow-y-auto {
height: 60vh;
}
/*generated with Input range slider CSS style generator (version 20211225) /*generated with Input range slider CSS style generator (version 20211225)
https://toughengineer.github.io/demo/slider-styler*/ https://toughengineer.github.io/demo/slider-styler*/
:root { :root {
@ -62,6 +67,7 @@ https://toughengineer.github.io/demo/slider-styler*/
--slider-border-radius: 1em; --slider-border-radius: 1em;
--slider-progress-background: #8652bb; --slider-progress-background: #8652bb;
} }
input[type=range].styled-slider { input[type=range].styled-slider {
height: var(--slider-height); height: var(--slider-height);
-webkit-appearance: none; -webkit-appearance: none;
@ -101,7 +107,7 @@ input[type=range].styled-slider::-webkit-slider-thumb:hover {
} }
input[type=range].styled-slider.slider-progress::-webkit-slider-runnable-track { input[type=range].styled-slider.slider-progress::-webkit-slider-runnable-track {
background: linear-gradient(var(--slider-progress-background),var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B; background: linear-gradient(var(--slider-progress-background), var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
} }
/*mozilla*/ /*mozilla*/
@ -127,7 +133,7 @@ input[type=range].styled-slider::-moz-range-thumb:hover {
} }
input[type=range].styled-slider.slider-progress::-moz-range-track { input[type=range].styled-slider.slider-progress::-moz-range-track {
background: linear-gradient(var(--slider-progress-background),var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B; background: linear-gradient(var(--slider-progress-background), var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
} }
/*ms*/ /*ms*/
@ -172,4 +178,4 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower {
background: var(--slider-progress-background); background: var(--slider-progress-background);
border: none; border: none;
border-right-width: 0; border-right-width: 0;
} }

View File

@ -63,12 +63,15 @@
"captions": "Captions", "captions": "Captions",
"download": "Download", "download": "Download",
"settings": "Settings", "settings": "Settings",
"pictureInPicture": "Picture in Picture" "pictureInPicture": "Picture in Picture",
"playbackSpeed": "Playback speed"
}, },
"popouts": { "popouts": {
"sources": "Sources", "sources": "Sources",
"seasons": "Seasons", "seasons": "Seasons",
"captions": "Captions", "captions": "Captions",
"playbackSpeed": "Playback speed",
"customPlaybackSpeed": "Custom playback speed",
"captionPreferences": { "captionPreferences": {
"title": "Customize", "title": "Customize",
"delay": "Delay", "delay": "Delay",
@ -82,6 +85,7 @@
"customCaption": "Custom caption", "customCaption": "Custom caption",
"uploadCustomCaption": "Upload caption", "uploadCustomCaption": "Upload caption",
"noEmbeds": "No embeds were found for this source", "noEmbeds": "No embeds were found for this source",
"errors": { "errors": {
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}", "loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
"embedsError": "Something went wrong loading the embeds for this thing that you like" "embedsError": "Something went wrong loading the embeds for this thing that you like"
@ -92,7 +96,8 @@
"seasons": "Choose which season you want to watch", "seasons": "Choose which season you want to watch",
"episode": "Pick an episode", "episode": "Pick an episode",
"captions": "Choose a subtitle language", "captions": "Choose a subtitle language",
"captionPreferences": "Make subtitles look how you want it" "captionPreferences": "Make subtitles look how you want it",
"playbackSpeed": "Change the playback speed"
} }
}, },
"errors": { "errors": {

View File

@ -0,0 +1,17 @@
import { Icons } from "@/components/Icon";
import { useTranslation } from "react-i18next";
import { PopoutListAction } from "../../popouts/PopoutUtils";
interface Props {
onClick: () => any;
}
export function PlaybackSpeedSelectionAction(props: Props) {
const { t } = useTranslation();
return (
<PopoutListAction icon={Icons.TACHOMETER} onClick={props.onClick}>
{t("videoPlayer.buttons.playbackSpeed")}
</PopoutListAction>
);
}

View File

@ -3,8 +3,8 @@ import { ErrorMessage } from "@/components/layout/ErrorBoundary";
import { Link } from "@/components/text/Link"; import { Link } from "@/components/text/Link";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { Component } from "react"; import { Component } from "react";
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
import { VideoPlayerHeader } from "./VideoPlayerHeader"; import { VideoPlayerHeader } from "./VideoPlayerHeader";
interface ErrorBoundaryState { interface ErrorBoundaryState {

View File

@ -3,52 +3,9 @@ import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter"; import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { useSettings } from "@/state/settings"; import { useSettings } from "@/state/settings";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChangeEventHandler, useEffect, useRef } from "react";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { Slider } from "@/components/Slider";
export type SliderProps = {
label: string;
min: number;
max: number;
step: number;
value: number;
valueDisplay?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
};
export function Slider(props: SliderProps) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
const e = ref.current as HTMLInputElement;
e.style.setProperty("--value", e.value);
e.style.setProperty("--min", e.min === "" ? "0" : e.min);
e.style.setProperty("--max", e.max === "" ? "100" : e.max);
e.addEventListener("input", () => e.style.setProperty("--value", e.value));
}, [ref]);
return (
<div className="mb-6 flex flex-row gap-4">
<div className="flex w-full flex-col gap-2">
<label className="font-bold">{props.label}</label>
<input
type="range"
ref={ref}
className="styled-slider slider-progress"
onChange={props.onChange}
value={props.value}
max={props.max}
min={props.min}
step={props.step}
/>
</div>
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
<div className="text-center font-bold text-white">
{props.valueDisplay ?? props.value}
</div>
</div>
</div>
);
}
export function CaptionSettingsPopout(props: { export function CaptionSettingsPopout(props: {
router: ReturnType<typeof useFloatingRouter>; router: ReturnType<typeof useFloatingRouter>;
@ -73,7 +30,7 @@ export function CaptionSettingsPopout(props: {
/> />
<FloatingCardView.Content> <FloatingCardView.Content>
<Slider <Slider
label={t("videoPlayer.popouts.captionPreferences.delay")} label={t("videoPlayer.popouts.captionPreferences.delay") as string}
max={10} max={10}
min={-10} min={-10}
step={0.1} step={0.1}
@ -90,7 +47,7 @@ export function CaptionSettingsPopout(props: {
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)} onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
/> />
<Slider <Slider
label={t("videoPlayer.popouts.captionPreferences.opacity")} label={t("videoPlayer.popouts.captionPreferences.opacity") as string}
step={1} step={1}
min={0} min={0}
max={255} max={255}

View File

@ -0,0 +1,73 @@
import { Icon, Icons } from "@/components/Icon";
import { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useControls } from "@/video/state/logic/controls";
import { useTranslation } from "react-i18next";
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
import { Slider } from "@/components/Slider";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
const speedSelectionOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2];
export function PlaybackSpeedPopout(props: {
router: ReturnType<typeof useFloatingRouter>;
prefix: string;
}) {
const { t } = useTranslation();
const descriptor = useVideoPlayerDescriptor();
const controls = useControls(descriptor);
const mediaPlaying = useMediaPlaying(descriptor);
return (
<FloatingView
{...props.router.pageProps(props.prefix)}
width={320}
height={500}
>
<FloatingCardView.Header
title={t("videoPlayer.popouts.playbackSpeed")}
description={t("videoPlayer.popouts.descriptions.playbackSpeed")}
goBack={() => props.router.navigate("/")}
/>
<FloatingCardView.Content noSection>
<PopoutSection>
{speedSelectionOptions.map((speed) => (
<PopoutListEntry
key={speed}
active={mediaPlaying.playbackSpeed === speed}
onClick={() => {
controls.setPlaybackSpeed(speed);
controls.closePopout();
}}
>
{speed}x
</PopoutListEntry>
))}
</PopoutSection>
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-300 px-5 py-3 text-xs font-bold uppercase">
<Icon className="text-base" icon={Icons.TACHOMETER} />
<span>{t("videoPlayer.popouts.customPlaybackSpeed")}</span>
</p>
<PopoutSection className="pt-0">
<div>
<Slider
min={0.1}
max={10}
step={0.1}
value={mediaPlaying.playbackSpeed}
valueDisplay={`${mediaPlaying.playbackSpeed}x`}
onChange={(e: { target: { valueAsNumber: number } }) =>
controls.setPlaybackSpeed(e.target.valueAsNumber)
}
/>
</div>
</PopoutSection>
</FloatingCardView.Content>
</FloatingView>
);
}

View File

@ -5,9 +5,11 @@ import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction"; import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
import { CaptionsSelectionAction } from "@/video/components/actions/list-entries/CaptionsSelectionAction"; import { CaptionsSelectionAction } from "@/video/components/actions/list-entries/CaptionsSelectionAction";
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction"; import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
import { PlaybackSpeedSelectionAction } from "@/video/components/actions/list-entries/PlaybackSpeedSelectionAction";
import { CaptionSelectionPopout } from "./CaptionSelectionPopout"; import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
import { SourceSelectionPopout } from "./SourceSelectionPopout"; import { SourceSelectionPopout } from "./SourceSelectionPopout";
import { CaptionSettingsPopout } from "./CaptionSettingsPopout"; import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
import { PlaybackSpeedPopout } from "./PlaybackSpeedPopout";
export function SettingsPopout() { export function SettingsPopout() {
const floatingRouter = useFloatingRouter(); const floatingRouter = useFloatingRouter();
@ -21,6 +23,9 @@ export function SettingsPopout() {
<DownloadAction /> <DownloadAction />
<SourceSelectionAction onClick={() => navigate("/source")} /> <SourceSelectionAction onClick={() => navigate("/source")} />
<CaptionsSelectionAction onClick={() => navigate("/captions")} /> <CaptionsSelectionAction onClick={() => navigate("/captions")} />
<PlaybackSpeedSelectionAction
onClick={() => navigate("/playback-speed")}
/>
</FloatingCardView.Content> </FloatingCardView.Content>
</FloatingView> </FloatingView>
<SourceSelectionPopout router={floatingRouter} prefix="source" /> <SourceSelectionPopout router={floatingRouter} prefix="source" />
@ -29,6 +34,7 @@ export function SettingsPopout() {
router={floatingRouter} router={floatingRouter}
prefix="caption-settings" prefix="caption-settings"
/> />
<PlaybackSpeedPopout router={floatingRouter} prefix="playback-speed" />
</> </>
); );
} }

View File

@ -13,6 +13,7 @@ export function resetForSource(s: VideoPlayerState) {
isFirstLoading: true, isFirstLoading: true,
hasPlayedOnce: false, hasPlayedOnce: false,
volume: state.mediaPlaying.volume, // volume settings needs to persist through resets volume: state.mediaPlaying.volume, // volume settings needs to persist through resets
playbackSpeed: 1,
}; };
state.progress = { state.progress = {
time: 0, time: 0,
@ -42,6 +43,7 @@ function initPlayer(): VideoPlayerState {
isFirstLoading: true, isFirstLoading: true,
hasPlayedOnce: false, hasPlayedOnce: false,
volume: 0, volume: 0,
playbackSpeed: 1,
}, },
progress: { progress: {

View File

@ -14,6 +14,7 @@ export type ControlMethods = {
setCurrentEpisode(sId: string, eId: string): void; setCurrentEpisode(sId: string, eId: string): void;
setDraggingTime(num: number): void; setDraggingTime(num: number): void;
togglePictureInPicture(): void; togglePictureInPicture(): void;
setPlaybackSpeed(num: number): void;
}; };
export function useControls( export function useControls(
@ -105,5 +106,9 @@ export function useControls(
state.stateProvider?.togglePictureInPicture(); state.stateProvider?.togglePictureInPicture();
updateInterface(descriptor, state); updateInterface(descriptor, state);
}, },
setPlaybackSpeed(num) {
state.stateProvider?.setPlaybackSpeed(num);
updateInterface(descriptor, state);
},
}; };
} }

View File

@ -12,6 +12,7 @@ export type VideoMediaPlayingEvent = {
hasPlayedOnce: boolean; hasPlayedOnce: boolean;
isFirstLoading: boolean; isFirstLoading: boolean;
volume: number; volume: number;
playbackSpeed: number;
}; };
function getMediaPlayingFromState( function getMediaPlayingFromState(
@ -26,6 +27,7 @@ function getMediaPlayingFromState(
isDragSeeking: state.mediaPlaying.isDragSeeking, isDragSeeking: state.mediaPlaying.isDragSeeking,
isFirstLoading: state.mediaPlaying.isFirstLoading, isFirstLoading: state.mediaPlaying.isFirstLoading,
volume: state.mediaPlaying.volume, volume: state.mediaPlaying.volume,
playbackSpeed: state.mediaPlaying.playbackSpeed,
}; };
} }

View File

@ -87,6 +87,23 @@ export function createCastingStateProvider(
togglePictureInPicture() { togglePictureInPicture() {
// no picture in picture while casting // no picture in picture while casting
}, },
setPlaybackSpeed(num) {
const mediaInfo = new chrome.cast.media.MediaInfo(
state.meta?.meta.meta.id ?? "video",
"video/mp4"
);
(mediaInfo as any).contentUrl = state.source?.url;
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
mediaInfo.metadata = new chrome.cast.media.MovieMediaMetadata();
mediaInfo.metadata.title = state.meta?.meta.meta.title ?? "";
mediaInfo.customData = {
playbackRate: num,
};
const request = new chrome.cast.media.LoadRequest(mediaInfo);
request.autoplay = true;
const session = ins?.getCurrentSession();
session?.loadMedia(request);
},
async setVolume(v) { async setVolume(v) {
// clamp time between 0 and 1 // clamp time between 0 and 1
let volume = Math.min(v, 1); let volume = Math.min(v, 1);
@ -114,7 +131,7 @@ export function createCastingStateProvider(
movieMeta.title = state.meta?.meta.meta.title ?? ""; movieMeta.title = state.meta?.meta.meta.title ?? "";
const mediaInfo = new chrome.cast.media.MediaInfo( const mediaInfo = new chrome.cast.media.MediaInfo(
state.meta?.meta.meta.id ?? "hello", state.meta?.meta.meta.id ?? "video",
"video/mp4" "video/mp4"
); );
(mediaInfo as any).contentUrl = source?.source; (mediaInfo as any).contentUrl = source?.source;

View File

@ -22,6 +22,7 @@ export type VideoPlayerStateController = {
clearCaption(): void; clearCaption(): void;
getId(): string; getId(): string;
togglePictureInPicture(): void; togglePictureInPicture(): void;
setPlaybackSpeed(num: number): void;
}; };
export type VideoPlayerStateProvider = VideoPlayerStateController & { export type VideoPlayerStateProvider = VideoPlayerStateController & {

View File

@ -228,6 +228,11 @@ export function createVideoStateProvider(
} }
} }
}, },
setPlaybackSpeed(num) {
player.playbackRate = num;
state.mediaPlaying.playbackSpeed = num;
updateMediaPlaying(descriptor, state);
},
providerStart() { providerStart() {
this.setVolume(getStoredVolume()); this.setVolume(getStoredVolume());
@ -276,6 +281,10 @@ export function createVideoStateProvider(
state.mediaPlaying.isLoading = false; state.mediaPlaying.isLoading = false;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const ratechange = () => {
state.mediaPlaying.playbackSpeed = player.playbackRate;
updateMediaPlaying(descriptor, state);
};
const fullscreenchange = () => { const fullscreenchange = () => {
state.interface.isFullscreen = state.interface.isFullscreen =
!!document.fullscreenElement || // other browsers !!document.fullscreenElement || // other browsers
@ -326,6 +335,7 @@ export function createVideoStateProvider(
player.addEventListener("timeupdate", timeupdate); player.addEventListener("timeupdate", timeupdate);
player.addEventListener("loadedmetadata", loadedmetadata); player.addEventListener("loadedmetadata", loadedmetadata);
player.addEventListener("canplay", canplay); player.addEventListener("canplay", canplay);
player.addEventListener("ratechange", ratechange);
fscreen.addEventListener("fullscreenchange", fullscreenchange); fscreen.addEventListener("fullscreenchange", fullscreenchange);
player.addEventListener("error", error); player.addEventListener("error", error);
player.addEventListener( player.addEventListener(

View File

@ -42,6 +42,7 @@ export type VideoPlayerState = {
isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing
hasPlayedOnce: boolean; // has the video played at all? hasPlayedOnce: boolean; // has the video played at all?
volume: number; volume: number;
playbackSpeed: number;
}; };
// state related to video progress // state related to video progress