Skip to content

Commit

Permalink
refactor(player): impl remote tracks fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed Mar 21, 2024
1 parent 3d88e34 commit 1174e3d
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 116 deletions.
126 changes: 46 additions & 80 deletions apps/app/src/components/player.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import "@vidstack/react/player/styles/base.css";

import type { MediaViewType, PlayerSrc } from "@vidstack/react";
import { MediaPlayer, Track, useMediaState } from "@vidstack/react";
import type { MediaErrorDetail, MediaViewType } from "@vidstack/react";
import { MediaPlayer, useMediaState } from "@vidstack/react";

import { Notice } from "obsidian";
import { useMemo, useState } from "react";
import { useState } from "react";
import { useTempFragHandler } from "@/components/hook/use-temporal-frag";
import { encodeWebpageUrl } from "@/lib/remote-player/encode";
import { cn } from "@/lib/utils";
import { toInfoKey } from "@/media-note/note-index/def";
import { isFileMediaInfo } from "../info/media-info";
import { useApp, useIsEmbed, useMediaViewStore, useSettings } from "./context";
import { useIsEmbed, useMediaViewStore, useSettings } from "./context";
import { useViewTypeDetect } from "./hook/fix-webm-audio";
import { useControls, useDefaultVolume, useHashProps } from "./hook/use-hash";
import { useAutoContinuePlay } from "./hook/use-playlist";
import { AudioLayout } from "./player/layouts/audio-layout";
import { VideoLayout } from "./player/layouts/video-layout";
import { MediaProviderEnhanced } from "./provider";
import { useSource } from "./use-source";
import { useRemoteTracks } from "./use-tracks";

function HookLoader({
onViewTypeChange,
}: {
onViewTypeChange: (viewType: "audio" | "unknown") => any;
}) {
useViewTypeDetect(onViewTypeChange);
useRemoteTracks();
useTempFragHandler();
useDefaultVolume();
return <></>;
Expand All @@ -38,36 +39,6 @@ function PlayerLayout() {
return <VideoLayout />;
}

function useSource() {
const mediaInfo = useMediaViewStore((s) => s.source?.url);
const { vault } = useApp();
const mediaInfoKey = mediaInfo ? toInfoKey(mediaInfo) : null;
const src = useMemo(() => {
if (!mediaInfo) return;
if (isFileMediaInfo(mediaInfo)) {
return vault.getResourcePath(mediaInfo.file);
}
return mediaInfo.source.href;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mediaInfoKey]);

const type = useMediaViewStore(
(s): "video/mp4" | "audio/mp3" | "webpage" | undefined => {
const viewType = s.source?.viewType;
if (!viewType) return;
if (viewType === "mx-webpage") return "webpage";
if (viewType?.endsWith("video")) return "video/mp4";
if (viewType?.endsWith("audio")) return "audio/mp3";
},
);
if (!src) return;

if (type === "webpage") {
return { src: encodeWebpageUrl(src) } satisfies PlayerSrc;
}
return { src, type } satisfies PlayerSrc;
}

export function Player() {
const playerRef = useMediaViewStore((s) => s.playerRef);
const { onEnded } = useAutoContinuePlay();
Expand All @@ -79,7 +50,6 @@ export function Player() {
}
return source.url.source.pathname.endsWith(".webm");
});
const textTracks = useMediaViewStore(({ textTracks }) => textTracks);
const load = useSettings((s) => s.loadStrategy);
const isEmbed = useIsEmbed();

Expand All @@ -101,51 +71,10 @@ export function Player() {
viewType={viewType}
ref={playerRef}
onEnded={onEnded}
onError={(e) => {
new Notice(
createFragment((frag) => {
frag.appendText(`Failed to load media for ${source.src}: `);
frag.createEl("br");
switch (e.code) {
// MEDIA_ERR_ABORTED
case 1:
frag.appendText("The media playback was aborted");
break;
// MEDIA_ERR_NETWORK
case 2:
frag.appendText(
"A network error caused the media playback to fail",
);
break;
// MEDIA_ERR_DECODE
case 3:
frag.appendText(
"The media playback was aborted due to a corruption problem or because the media encoding is not supported",
);
break;
// MEDIA_ERR_SRC_NOT_SUPPORTED
case 4:
frag.appendText(
"The media is not supported to open as regular video or audio, try open as webpage",
);
break;
default:
frag.appendText(
e.message || "Unknown error, check console for more details",
);
console.error("Failed to load media", source.src, e);
break;
}
}),
);
}}
onError={(e) => handleError(e, source.src)}
{...hashProps}
>
<MediaProviderEnhanced>
{textTracks.map((props) => (
<Track {...props} key={props.id} />
))}
</MediaProviderEnhanced>
<MediaProviderEnhanced></MediaProviderEnhanced>
<HookLoader
onViewTypeChange={(viewType) => {
setViewType(viewType);
Expand All @@ -160,3 +89,40 @@ export function Player() {
</MediaPlayer>
);
}

function handleError(e: MediaErrorDetail, source: URL | string) {
new Notice(
createFragment((frag) => {
frag.appendText(`Failed to load media for ${source}: `);
frag.createEl("br");
switch (e.code) {
// MEDIA_ERR_ABORTED
case 1:
frag.appendText("The media playback was aborted");
break;
// MEDIA_ERR_NETWORK
case 2:
frag.appendText("A network error caused the media playback to fail");
break;
// MEDIA_ERR_DECODE
case 3:
frag.appendText(
"The media playback was aborted due to a corruption problem or because the media encoding is not supported",
);
break;
// MEDIA_ERR_SRC_NOT_SUPPORTED
case 4:
frag.appendText(
"The media is not supported to open as regular video or audio, try open as webpage",
);
break;
default:
frag.appendText(
e.message || "Unknown error, check console for more details",
);
console.error("Failed to load media", source, e);
break;
}
}),
);
}
67 changes: 67 additions & 0 deletions apps/app/src/components/player/caption-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { CaptionOption } from "@vidstack/react";
import { useCaptionOptions, useMediaState } from "@vidstack/react";
import { SubtitlesIcon } from "@/components/icon";
import { dataLpPassthrough } from "./buttons";
import { useMenu } from "./menus";

function sortCaption(a: CaptionOption, b: CaptionOption) {
if (a.track && b.track) {
// if with item.track.language, sort by item.track.language
if (a.track.language && b.track.language) {
return (a.track as { language: string }).language.localeCompare(
(b.track as { language: string }).language,
);
}

// if not with item.track.language, sort by item.label
if (!a.track.language && !b.track.language) {
return a.label.localeCompare(b.label);
}

// if one has language and the other doesn't, the one with language comes first
if (a.track.language && !b.track.language) {
return -1;
}
if (!a.track.language && b.track.language) {
return 1;
}
}
// if item.track is null, put it at the end
if (a.track === null && b.track !== null) {
return 1;
}
if (a.track !== null && b.track === null) {
return -1;
}
return 0;
}
export function Captions() {
const options = useCaptionOptions();
const tracks = useMediaState("textTracks");
const onClick = useMenu((menu) => {
options
.sort(sortCaption)
.forEach(({ label, select, selected }, idx, options) => {
menu.addItem((item) => {
if (options.length === 2 && label === "Unknown") {
label = "On";
}
item.setTitle(label).setChecked(selected).onClick(select);
});
});
return true;
});

if (tracks.length === 0) return null;

return (
<button
className="group ring-mod-border-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-2 aria-disabled:hidden"
{...{ [dataLpPassthrough]: true }}
onClick={onClick}
aria-label="Select Caption"
>
<SubtitlesIcon className="w-7 h-7" />
</button>
);
}
39 changes: 4 additions & 35 deletions apps/app/src/components/player/menus.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import "./menu.css";

import {
useCaptionOptions,
useMediaPlayer,
useMediaState,
} from "@vidstack/react";
import { useMediaPlayer } from "@vidstack/react";
import { around } from "monkey-around";
import type { MenuItem } from "obsidian";
import { Menu } from "obsidian";
import { useRef } from "react";
import { MoreIcon, PlaylistIcon, SubtitlesIcon } from "@/components/icon";
import { MoreIcon, PlaylistIcon } from "@/components/icon";
import { showAtButton } from "@/lib/menu";
import { compare } from "@/media-note/note-index/def";
import type { PlaylistItem } from "@/media-note/playlist/def";
Expand All @@ -27,7 +23,7 @@ import { usePlaylist } from "../hook/use-playlist";
import { dataLpPassthrough } from "./buttons";
import { addItemsToMenu } from "./playlist-menu";

function useMenu(onMenu: (menu: Menu) => boolean) {
export function useMenu(onMenu: (menu: Menu) => boolean) {
const menuRef = useRef<Menu | null>(null);
return (evt: React.MouseEvent) => {
menuRef.current?.close();
Expand Down Expand Up @@ -123,34 +119,7 @@ export function Playlist() {
);
}

export function Captions() {
const options = useCaptionOptions();
const tracks = useMediaState("textTracks");
const onClick = useMenu((menu) => {
options.forEach(({ label, select, selected }, idx, options) => {
menu.addItem((item) => {
if (options.length === 2 && label === "Unknown") {
label = "On";
}
item.setTitle(label).setChecked(selected).onClick(select);
});
});
return true;
});

if (tracks.length === 0) return null;

return (
<button
className="group ring-mod-border-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-2 aria-disabled:hidden"
{...{ [dataLpPassthrough]: true }}
onClick={onClick}
aria-label="Select Caption"
>
<SubtitlesIcon className="w-7 h-7" />
</button>
);
}
export { Captions } from "./caption-menu";

export function MoreOptions() {
const player = useMediaPlayer();
Expand Down
9 changes: 8 additions & 1 deletion apps/app/src/components/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
type MediaProviderInstance,
type MediaProviderLoader,
type MediaProviderProps,
Track,
} from "@vidstack/react";
import { useCallback } from "react";
import { getPartition } from "@/lib/remote-player/const";
import { WebviewProviderLoader } from "@/lib/remote-player/loader";
import { cn } from "@/lib/utils";
import { useApp, useMediaViewStore } from "./context";
import { useControls } from "./hook/use-hash";
import { useTracks } from "./use-tracks";
import { WebView } from "./webview";

export function MediaProviderEnhanced({
Expand All @@ -38,6 +40,7 @@ export function MediaProviderEnhanced({
}
});
const controls = useControls();
const tracks = useTracks();
return (
<MediaProvider
className={cn(
Expand Down Expand Up @@ -71,6 +74,10 @@ export function MediaProviderEnhanced({
[appId, controls],
)}
{...props}
/>
>
{tracks.map((props) => (
<Track {...props} key={props.id} />
))}
</MediaProvider>
);
}
36 changes: 36 additions & 0 deletions apps/app/src/components/use-source.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { PlayerSrc } from "@vidstack/react";
import { useMemo } from "react";
import { encodeWebpageUrl } from "@/lib/remote-player/encode";
import { toInfoKey } from "@/media-note/note-index/def";
import { isFileMediaInfo } from "../info/media-info";
import { useApp, useMediaViewStore } from "./context";

export function useSource() {
const mediaInfo = useMediaViewStore((s) => s.source?.url);
const { vault } = useApp();
const mediaInfoKey = mediaInfo ? toInfoKey(mediaInfo) : null;
const src = useMemo(() => {
if (!mediaInfo) return;
if (isFileMediaInfo(mediaInfo)) {
return vault.getResourcePath(mediaInfo.file);
}
return mediaInfo.source.href;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mediaInfoKey]);

const type = useMediaViewStore(
(s): "video/mp4" | "audio/mp3" | "webpage" | undefined => {
const viewType = s.source?.viewType;
if (!viewType) return;
if (viewType === "mx-webpage") return "webpage";
if (viewType?.endsWith("video")) return "video/mp4";
if (viewType?.endsWith("audio")) return "audio/mp3";
},
);
if (!src) return;

if (type === "webpage") {
return { src: encodeWebpageUrl(src) } satisfies PlayerSrc;
}
return { src, type } satisfies PlayerSrc;
}
Loading

0 comments on commit 1174e3d

Please sign in to comment.