Skip to content

Commit

Permalink
refactor youtube player
Browse files Browse the repository at this point in the history
  • Loading branch information
nknapp committed Aug 18, 2024
1 parent 0e09267 commit 8dd8e9b
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 119 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
coverage/
build/
playwright-report

52 changes: 52 additions & 0 deletions src/YoutubePlayer/PlayerContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { type Component, createSignal } from "solid-js";
import { SimpleButton } from "@/components/solid/atoms/SimpleButton.tsx";
import { render } from "solid-js/web";
import { t } from "@/i18n";
import { IconStop } from "@/icons";
import { cls } from "$core/utils/cls.ts";

export async function renderPlayerContainer() {
const container = document.createElement("div");
document.body.append(container);
const playerContainerRef = Promise.withResolvers<PlayerContainerRef>();
render(() => <PlayerContainer setPlayer={playerContainerRef.resolve} />, container);
return await playerContainerRef.promise;
}

export class PlayerContainerRef extends EventTarget {
constructor(
public htmlElement: HTMLDivElement,
public setVisible: (visible: boolean) => void,
) {
super();
}
}

const PlayerContainer: Component<{
setPlayer: (container: PlayerContainerRef) => void;
}> = (props) => {
let playerContainerRef: PlayerContainerRef | null = null;
const [visible, setVisible] = createSignal(false);

function onPlayerElement(el: HTMLDivElement) {
playerContainerRef = new PlayerContainerRef(el, setVisible);
props.setPlayer(playerContainerRef);
}

function stop() {
playerContainerRef?.dispatchEvent(new Event("stop"));
}

return (
<div class={cls("fixed inset-0 bg-primary-dark", visible() ? "visible" : "hidden")}>
<SimpleButton
class={"absolute top-1 right-1 z-10"}
size={"small"}
label={t("video.stop")}
icon={IconStop}
onClick={stop}
/>
<div ref={onPlayerElement} class="absolute inset-0 z-0"></div>
</div>
);
};
72 changes: 72 additions & 0 deletions src/YoutubePlayer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { YoutubeLink } from "$core/model";
import { renderPlayerContainer } from "@/YoutubePlayer/PlayerContainer.tsx";

export interface YoutubePlayer {
loadVideo(videoId: string): Promise<void>;
play(): Promise<void>;
stop(): Promise<void>;
waitForStop(): Promise<void>;
}

export async function playVideo(youtubeLink: YoutubeLink): Promise<void> {
const player = await getOrCreatePlayer();
await player.loadVideo(youtubeLink.videoId);
await player.play();
await player.waitForStop();
await player.stop();
}

let cachedPlayer: Promise<YoutubePlayer> | null = null;

export function getOrCreatePlayer(): Promise<YoutubePlayer> {
cachedPlayer = cachedPlayer ?? createPlayer();
return cachedPlayer;
}

async function createPlayer(): Promise<YoutubePlayer> {
const container = await renderPlayerContainer();
const { default: Player } = await import("youtube-player");
const player = Player(container.htmlElement, {
host: "https://www.youtube-nocookie.com",
playerVars: {
rel: 0,
autoplay: 0,
modestbranding: 1,
},
});

function updatePlayerSize() {
player?.setSize(window.innerWidth, window.innerHeight);
}

window.addEventListener("resize", updatePlayerSize);
updatePlayerSize();

const result = {
loadVideo(videoId: string) {
return player.loadVideoById({ videoId });
},
async play() {
await player.playVideo();
container.setVisible(true);
},
async stop() {
await player.stopVideo();
container.setVisible(false);
},
async waitForStop() {
return new Promise<void>((resolve) => {
player.on("stateChange", (event) => {
if (event.data === 0) {
// Video has ended
resolve();
}
});
});
},
};
container.addEventListener("stop", () => {
result.stop();
});
return result;
}
37 changes: 37 additions & 0 deletions src/components/solid/atoms/YoutubePlayButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type Component, Match, Switch } from "solid-js";
import type { YoutubeLink } from "$core/model";
import { SimpleButton } from "@/components/solid/atoms/SimpleButton.tsx";
import { IconVideoLibrary } from "@/icons";
import { t } from "@/i18n";
import { playVideo } from "@/YoutubePlayer";

export interface YoutubePlayerProps {
link: YoutubeLink;
type: "button" | "icon";
class?: string;
}

export const YoutubePlayButton: Component<YoutubePlayerProps> = (props) => {
async function play() {
await playVideo(props.link);
}

return (
<Switch>
<Match when={props.type === "icon"}>
<button onClick={play}>
<IconVideoLibrary class={props.class} />
</button>
</Match>
<Match when={props.type === "button"}>
<SimpleButton
class={props.class}
size={"small"}
icon={IconVideoLibrary}
label={t("button.play-video.label")}
onClick={play}
></SimpleButton>
</Match>
</Switch>
);
};
107 changes: 0 additions & 107 deletions src/components/solid/atoms/YoutubePlayer.tsx

This file was deleted.

9 changes: 3 additions & 6 deletions src/components/solid/organisms/Reader/Reader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Component, createSignal, For, lazy, Suspense } from "solid-js";
import { type Component, createSignal, For, Suspense } from "solid-js";
import type { DojoInfo } from "$core/model/Dojo.ts";
import { createResource } from "solid-js";
import { createTechniqueStore } from "$core/store";
Expand All @@ -13,10 +13,7 @@ import { type Speed, SpeedButton } from "@/components/solid/organisms/Reader/Spe
import { type DelayControl, DelayIndicator } from "@/components/solid/atoms/DelayIndicator.tsx";
import { youtubeEnabled } from "$core/store/youtube.ts";
import { usePersistentStore } from "@/components/solid/hooks/usePersistentStore.ts";

const YoutubePlayer = lazy(() =>
import("@/components/solid/atoms/YoutubePlayer.tsx").then(({ YoutubePlayer }) => ({ default: YoutubePlayer })),
);
import { YoutubePlayButton } from "@/components/solid/atoms/YoutubePlayButton.tsx";

export const Reader: Component<{ dojoInfo: DojoInfo; speechPack: SpeechPack }> = (props) => {
const techniqueStore = createTechniqueStore(props.dojoInfo.id);
Expand Down Expand Up @@ -128,7 +125,7 @@ const Player: Component<{
<div class={"h-8 w-full flex"}>
<For each={props.youtube}>
{(link) => {
return <YoutubePlayer type="button" class={"flex-1"} link={link} />;
return <YoutubePlayButton type="button" class={"flex-1"} link={link} />;
}}
</For>
</div>
Expand Down
9 changes: 3 additions & 6 deletions src/components/solid/organisms/TechniqueChooser/ExamSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { type Component, createMemo, For, lazy, Match, Show, Switch } from "solid-js";
import { type Component, createMemo, For, Match, Show, Switch } from "solid-js";
import { SINGLE_DIRECTION, type Technique, type TechniqueMetadata, type YoutubeLink } from "$core/model";
import { ForEntries } from "./ForEntries.tsx";
import { buildExamTable } from "$core/buildExamTable";
import { t } from "@/i18n";
import { youtubeEnabled } from "$core/store/youtube.ts";
import { usePersistentStore } from "@/components/solid/hooks/usePersistentStore.ts";

const YoutubePlayer = lazy(() =>
import("@/components/solid/atoms/YoutubePlayer.tsx").then(({ YoutubePlayer }) => ({ default: YoutubePlayer })),
);
import { YoutubePlayButton } from "@/components/solid/atoms/YoutubePlayButton.tsx";

export interface ExamSheetProps {
techniques: Technique[];
Expand Down Expand Up @@ -92,7 +89,7 @@ const YoutubeLink: Component<{ metadata: TechniqueMetadata }> = (props) => {
return (
<Show when={showYoutube()}>
<span class={"print:hidden"}>
<For each={youtube}>{(video) => <YoutubePlayer type={"icon"} class={"inline mx-2"} link={video} />}</For>
<For each={youtube}>{(video) => <YoutubePlayButton type={"icon"} class={"inline mx-2"} link={video} />}</For>
</span>
</Show>
);
Expand Down

0 comments on commit 8dd8e9b

Please sign in to comment.