44 changed files with 3542 additions and 149 deletions
@ -1,5 +1,8 @@
@@ -1,5 +1,8 @@
|
||||
{ |
||||
"editor.formatOnSave": true, |
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint", |
||||
"eslint.format.enable": true |
||||
"eslint.format.enable": true, |
||||
"[json]": { |
||||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
||||
} |
||||
} |
||||
|
@ -1,20 +0,0 @@
@@ -1,20 +0,0 @@
|
||||
{ |
||||
"name": "movie-web", |
||||
"short_name": "movie-web", |
||||
"icons": [ |
||||
{ |
||||
"src": "/android-chrome-192x192.png", |
||||
"sizes": "192x192", |
||||
"type": "image/png" |
||||
}, |
||||
{ |
||||
"src": "/android-chrome-512x512.png", |
||||
"sizes": "512x512", |
||||
"type": "image/png" |
||||
} |
||||
], |
||||
"theme_color": "#E880C5", |
||||
"background_color": "#16171D", |
||||
"display": "standalone", |
||||
"start_url": "/" |
||||
} |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
import { describe, it } from "vitest"; |
||||
import "@/backend"; |
||||
import { getProviders } from "@/backend/helpers/register"; |
||||
import { MWMediaType } from "@/backend/metadata/types"; |
||||
import { runProvider } from "@/backend/helpers/run"; |
||||
import { testData } from "@/__tests__/providers/testdata"; |
||||
|
||||
describe("providers", () => { |
||||
const providers = getProviders(); |
||||
|
||||
it("have at least one provider", ({ expect }) => { |
||||
expect(providers.length).toBeGreaterThan(0); |
||||
}); |
||||
|
||||
for (const provider of providers) { |
||||
describe(provider.displayName, () => { |
||||
it("must have at least one type", async ({ expect }) => { |
||||
expect(provider.type.length).toBeGreaterThan(0); |
||||
}); |
||||
|
||||
if (provider.type.includes(MWMediaType.MOVIE)) { |
||||
it("must work with movies", async ({ expect }) => { |
||||
const movie = testData.find((v) => v.meta.type === MWMediaType.MOVIE); |
||||
if (!movie) throw new Error("no movie to test with"); |
||||
const results = await runProvider(provider, { |
||||
media: movie, |
||||
progress() {}, |
||||
type: movie.meta.type as any, |
||||
}); |
||||
expect(results).toBeTruthy(); |
||||
}); |
||||
} |
||||
|
||||
if (provider.type.includes(MWMediaType.SERIES)) { |
||||
it("must work with series", async ({ expect }) => { |
||||
const show = testData.find((v) => v.meta.type === MWMediaType.SERIES); |
||||
if (show?.meta.type !== MWMediaType.SERIES) |
||||
throw new Error("no show to test with"); |
||||
const results = await runProvider(provider, { |
||||
media: show, |
||||
progress() {}, |
||||
type: show.meta.type as MWMediaType.SERIES, |
||||
episode: show.meta.seasonData.episodes[0].id, |
||||
season: show.meta.seasons[0].id, |
||||
}); |
||||
expect(results).toBeTruthy(); |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
}); |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
import { DetailedMeta } from "@/backend/metadata/getmeta"; |
||||
import { MWMediaType } from "@/backend/metadata/types"; |
||||
|
||||
export const testData: DetailedMeta[] = [ |
||||
{ |
||||
imdbId: "tt10954562", |
||||
tmdbId: "572716", |
||||
meta: { |
||||
id: "439596", |
||||
title: "Hamilton", |
||||
type: MWMediaType.MOVIE, |
||||
year: "2020", |
||||
seasons: undefined, |
||||
}, |
||||
}, |
||||
{ |
||||
imdbId: "tt11126994", |
||||
tmdbId: "94605", |
||||
meta: { |
||||
id: "222333", |
||||
title: "Arcane", |
||||
type: MWMediaType.SERIES, |
||||
year: "2021", |
||||
seasons: [ |
||||
{ |
||||
id: "230301", |
||||
number: 1, |
||||
title: "Season 1", |
||||
}, |
||||
], |
||||
seasonData: { |
||||
id: "230301", |
||||
number: 1, |
||||
title: "Season 1", |
||||
episodes: [ |
||||
{ |
||||
id: "4243445", |
||||
number: 1, |
||||
title: "Welcome to the Playground", |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
}, |
||||
]; |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import { Icon, Icons } from "@/components/Icon"; |
||||
import { useBanner } from "@/hooks/useBanner"; |
||||
|
||||
export function Banner(props: { children: React.ReactNode; type: "error" }) { |
||||
const [ref] = useBanner<HTMLDivElement>("internet"); |
||||
const styles = { |
||||
error: "bg-[#C93957] text-white", |
||||
}; |
||||
const icons = { |
||||
error: Icons.CIRCLE_EXCLAMATION, |
||||
}; |
||||
|
||||
return ( |
||||
<div ref={ref}> |
||||
<div |
||||
className={[ |
||||
styles[props.type], |
||||
"flex items-center justify-center p-1", |
||||
].join(" ")} |
||||
> |
||||
<div className="flex items-center space-x-3"> |
||||
<Icon icon={icons[props.type]} /> |
||||
<div>{props.children}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
import { |
||||
ReactNode, |
||||
createContext, |
||||
useState, |
||||
useMemo, |
||||
Dispatch, |
||||
SetStateAction, |
||||
useEffect, |
||||
useContext, |
||||
} from "react"; |
||||
import { useMeasure } from "react-use"; |
||||
|
||||
interface BannerInstance { |
||||
id: string; |
||||
height: number; |
||||
} |
||||
|
||||
const BannerContext = createContext< |
||||
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>] |
||||
>(null as any); |
||||
|
||||
export function BannerContextProvider(props: { children: ReactNode }) { |
||||
const [state, setState] = useState<BannerInstance[]>([]); |
||||
const memod = useMemo< |
||||
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>] |
||||
>(() => [state, setState], [state]); |
||||
|
||||
return ( |
||||
<BannerContext.Provider value={memod}> |
||||
{props.children} |
||||
</BannerContext.Provider> |
||||
); |
||||
} |
||||
|
||||
export function useBanner<T extends Element>(id: string) { |
||||
const [ref, { height }] = useMeasure<T>(); |
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, set] = useContext(BannerContext); |
||||
|
||||
useEffect(() => { |
||||
set((v) => [...v, { id, height: 0 }]); |
||||
set((value) => { |
||||
const v = value.find((item) => item.id === id); |
||||
if (v) { |
||||
v.height = height; |
||||
} |
||||
return value; |
||||
}); |
||||
return () => { |
||||
set((v) => v.filter((item) => item.id !== id)); |
||||
}; |
||||
}, [height, id, set]); |
||||
|
||||
return [ref]; |
||||
} |
||||
|
||||
export function useBannerSize() { |
||||
const [val] = useContext(BannerContext); |
||||
|
||||
return val.reduce((a, v) => a + v.height, 0); |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef, useState } from "react"; |
||||
|
||||
export function useIsOnline() { |
||||
const [online, setOnline] = useState<boolean | null>(true); |
||||
const ref = useRef<boolean>(true); |
||||
|
||||
useEffect(() => { |
||||
let counter = 0; |
||||
|
||||
let abort: null | AbortController = null; |
||||
const interval = setInterval(() => { |
||||
// if online try once every 10 iterations intead of every iteration
|
||||
counter += 1; |
||||
if (ref.current) { |
||||
if (counter < 10) return; |
||||
} |
||||
counter = 0; |
||||
|
||||
if (abort) abort.abort(); |
||||
abort = new AbortController(); |
||||
const signal = abort.signal; |
||||
fetch("/ping.txt", { signal }) |
||||
.then(() => { |
||||
setOnline(true); |
||||
ref.current = true; |
||||
}) |
||||
.catch((err) => { |
||||
if (err.name === "AbortError") return; |
||||
setOnline(false); |
||||
ref.current = false; |
||||
}); |
||||
}, 5000); |
||||
|
||||
return () => { |
||||
clearInterval(interval); |
||||
if (abort) abort.abort(); |
||||
}; |
||||
}, []); |
||||
|
||||
return online; |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
import { Banner } from "@/components/Banner"; |
||||
import { useBannerSize } from "@/hooks/useBanner"; |
||||
import { useIsOnline } from "@/hooks/usePing"; |
||||
import { ReactNode } from "react"; |
||||
import { useTranslation } from "react-i18next"; |
||||
|
||||
export function Layout(props: { children: ReactNode }) { |
||||
const { t } = useTranslation(); |
||||
const isOnline = useIsOnline(); |
||||
const bannerSize = useBannerSize(); |
||||
|
||||
return ( |
||||
<div> |
||||
<div className="fixed inset-x-0 z-[1000]"> |
||||
{!isOnline ? <Banner type="error">{t("errors.offline")}</Banner> : null} |
||||
</div> |
||||
<div |
||||
style={{ |
||||
paddingTop: `${bannerSize}px`, |
||||
}} |
||||
className="flex min-h-screen flex-col" |
||||
> |
||||
{props.children} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb"; |
||||
export const GITHUB_LINK = "https://github.com/movie-web/movie-web"; |
||||
export const APP_VERSION = "3.0.3"; |
||||
export const APP_VERSION = "3.0.4"; |
||||
export const GA_ID = "G-44YVXRL61C"; |
||||
|
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
export function normalizeTitle(title: string): string { |
||||
return title |
||||
.trim() |
||||
.toLowerCase() |
||||
.replace(/['":]/g, "") |
||||
.replace(/[^a-zA-Z0-9]+/g, "_"); |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
import { Icons } from "@/components/Icon"; |
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||
import { useSource } from "@/video/state/logic/source"; |
||||
import { MWStreamType } from "@/backend/helpers/streams"; |
||||
import { normalizeTitle } from "@/utils/normalizeTitle"; |
||||
import { useIsMobile } from "@/hooks/useIsMobile"; |
||||
import { useTranslation } from "react-i18next"; |
||||
import { useMeta } from "@/video/state/logic/meta"; |
||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; |
||||
|
||||
interface Props { |
||||
className?: string; |
||||
} |
||||
|
||||
export function DownloadAction(props: Props) { |
||||
const descriptor = useVideoPlayerDescriptor(); |
||||
const sourceInterface = useSource(descriptor); |
||||
const { isMobile } = useIsMobile(); |
||||
const { t } = useTranslation(); |
||||
const meta = useMeta(descriptor); |
||||
|
||||
const isHLS = sourceInterface.source?.type === MWStreamType.HLS; |
||||
|
||||
const title = meta?.meta.meta.title; |
||||
|
||||
return ( |
||||
<a |
||||
href={isHLS ? undefined : sourceInterface.source?.url} |
||||
rel="noreferrer" |
||||
target="_blank" |
||||
download={title ? normalizeTitle(title) : undefined} |
||||
> |
||||
<VideoPlayerIconButton |
||||
className={props.className} |
||||
icon={Icons.DOWNLOAD} |
||||
disabled={isHLS} |
||||
text={isMobile ? (t("videoPlayer.buttons.download") as string) : ""} |
||||
/> |
||||
</a> |
||||
); |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
import { Navigation } from "@/components/layout/Navigation"; |
||||
import { ThinContainer } from "@/components/layout/ThinContainer"; |
||||
import { ArrowLink } from "@/components/text/ArrowLink"; |
||||
import { Title } from "@/components/text/Title"; |
||||
|
||||
export function DeveloperView() { |
||||
return ( |
||||
<div className="py-48"> |
||||
<Navigation /> |
||||
<ThinContainer classNames="flex flex-col space-y-4"> |
||||
<Title className="mb-8">Developer tools</Title> |
||||
<ArrowLink |
||||
to="/dev/providers" |
||||
direction="right" |
||||
linkText="Provider tester" |
||||
/> |
||||
<ArrowLink |
||||
to="/dev/embeds" |
||||
direction="right" |
||||
linkText="Embed scraper tester" |
||||
/> |
||||
<ArrowLink to="/dev/video" direction="right" linkText="Video tester" /> |
||||
</ThinContainer> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
|
||||
import { MWEmbed, MWEmbedScraper, MWEmbedType } from "@/backend/helpers/embed"; |
||||
import { getEmbeds } from "@/backend/helpers/register"; |
||||
import { runEmbedScraper } from "@/backend/helpers/run"; |
||||
import { MWStream } from "@/backend/helpers/streams"; |
||||
import { Button } from "@/components/Button"; |
||||
import { Navigation } from "@/components/layout/Navigation"; |
||||
import { ArrowLink } from "@/components/text/ArrowLink"; |
||||
import { Title } from "@/components/text/Title"; |
||||
import { useLoading } from "@/hooks/useLoading"; |
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; |
||||
|
||||
interface MediaSelectorProps { |
||||
embedType: MWEmbedType; |
||||
onSelect: (meta: MWEmbed) => void; |
||||
} |
||||
|
||||
interface EmbedScraperSelectorProps { |
||||
onSelect: (embedScraperId: string) => void; |
||||
} |
||||
|
||||
interface MediaScraperProps { |
||||
embed: MWEmbed; |
||||
scraper: MWEmbedScraper; |
||||
} |
||||
|
||||
function MediaSelector(props: MediaSelectorProps) { |
||||
const [url, setUrl] = useState(""); |
||||
|
||||
const select = useCallback( |
||||
(urlSt: string) => { |
||||
props.onSelect({ |
||||
type: props.embedType, |
||||
url: urlSt, |
||||
}); |
||||
}, |
||||
[props] |
||||
); |
||||
|
||||
return ( |
||||
<div className="flex flex-col space-y-4"> |
||||
<Title className="mb-8">Input embed url</Title> |
||||
<div className="mb-4 flex gap-4"> |
||||
<input |
||||
type="text" |
||||
placeholder="embed url here..." |
||||
value={url} |
||||
onChange={(e) => setUrl(e.target.value)} |
||||
/> |
||||
<Button onClick={() => select(url)}>Run scraper</Button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function MediaScraper(props: MediaScraperProps) { |
||||
const [results, setResults] = useState<MWStream | null>(null); |
||||
const [percentage, setPercentage] = useState(0); |
||||
|
||||
const [scrape, loading, error] = useLoading(async (url: string) => { |
||||
const data = await runEmbedScraper(props.scraper, { |
||||
url, |
||||
progress(num) { |
||||
console.log(`SCRAPING AT ${num}%`); |
||||
setPercentage(num); |
||||
}, |
||||
}); |
||||
console.log("got data", data); |
||||
setResults(data); |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
if (props.embed) { |
||||
scrape(props.embed.url); |
||||
} |
||||
}, [props.embed, scrape]); |
||||
|
||||
if (loading) return <p>Scraping... ({percentage}%)</p>; |
||||
if (error) return <p>Errored, check console</p>; |
||||
|
||||
return ( |
||||
<div> |
||||
<Title className="mb-8">Output data</Title> |
||||
<code> |
||||
<pre>{JSON.stringify(results, null, 2)}</pre> |
||||
</code> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function EmbedScraperSelector(props: EmbedScraperSelectorProps) { |
||||
const embedScrapers = getEmbeds(); |
||||
|
||||
return ( |
||||
<div className="flex flex-col space-y-4"> |
||||
<Title className="mb-8">Choose embed scraper</Title> |
||||
{embedScrapers.map((v) => ( |
||||
<ArrowLink |
||||
key={v.id} |
||||
onClick={() => props.onSelect(v.id)} |
||||
direction="right" |
||||
linkText={v.displayName} |
||||
/> |
||||
))} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function EmbedTesterView() { |
||||
const [embed, setEmbed] = useState<MWEmbed | null>(null); |
||||
const [embedScraperId, setEmbedScraperId] = useState<string | null>(null); |
||||
const embedScraper = useMemo( |
||||
() => getEmbeds().find((v) => v.id === embedScraperId), |
||||
[embedScraperId] |
||||
); |
||||
|
||||
let content: ReactNode = null; |
||||
if (!embedScraperId || !embedScraper) { |
||||
content = <EmbedScraperSelector onSelect={(id) => setEmbedScraperId(id)} />; |
||||
} else if (!embed) { |
||||
content = ( |
||||
<MediaSelector |
||||
embedType={embedScraper.for} |
||||
onSelect={(v) => setEmbed(v)} |
||||
/> |
||||
); |
||||
} else { |
||||
content = <MediaScraper scraper={embedScraper} embed={embed} />; |
||||
} |
||||
|
||||
return ( |
||||
<div className="py-48"> |
||||
<Navigation /> |
||||
<div className="mx-8 overflow-x-auto">{content}</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
import { MWProviderScrapeResult } from "@/backend/helpers/provider"; |
||||
import { getProviders } from "@/backend/helpers/register"; |
||||
import { runProvider } from "@/backend/helpers/run"; |
||||
import { DetailedMeta } from "@/backend/metadata/getmeta"; |
||||
import { Navigation } from "@/components/layout/Navigation"; |
||||
import { ArrowLink } from "@/components/text/ArrowLink"; |
||||
import { Title } from "@/components/text/Title"; |
||||
import { useLoading } from "@/hooks/useLoading"; |
||||
import { testData } from "@/__tests__/providers/testdata"; |
||||
import { ReactNode, useEffect, useState } from "react"; |
||||
|
||||
interface MediaSelectorProps { |
||||
onSelect: (meta: DetailedMeta) => void; |
||||
} |
||||
|
||||
interface ProviderSelectorProps { |
||||
onSelect: (providerId: string) => void; |
||||
} |
||||
|
||||
interface MediaScraperProps { |
||||
media: DetailedMeta | null; |
||||
id: string; |
||||
} |
||||
|
||||
function MediaSelector(props: MediaSelectorProps) { |
||||
const options: DetailedMeta[] = testData; |
||||
|
||||
return ( |
||||
<div className="flex flex-col space-y-4"> |
||||
<Title className="mb-8">Choose media</Title> |
||||
{options.map((v) => ( |
||||
<ArrowLink |
||||
key={v.imdbId} |
||||
onClick={() => props.onSelect(v)} |
||||
direction="right" |
||||
linkText={`${v.meta.title} (${v.meta.type})`} |
||||
/> |
||||
))} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function MediaScraper(props: MediaScraperProps) { |
||||
const [results, setResults] = useState<MWProviderScrapeResult | null>(null); |
||||
const [percentage, setPercentage] = useState(0); |
||||
|
||||
const [scrape, loading, error] = useLoading(async (media: DetailedMeta) => { |
||||
const provider = getProviders().find((v) => v.id === props.id); |
||||
if (!provider) throw new Error("provider not found"); |
||||
const data = await runProvider(provider, { |
||||
progress(num) { |
||||
console.log(`SCRAPING AT ${num}%`); |
||||
setPercentage(num); |
||||
}, |
||||
media, |
||||
type: media.meta.type as any, |
||||
}); |
||||
console.log("got data", data); |
||||
setResults(data); |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
if (props.media) { |
||||
scrape(props.media); |
||||
} |
||||
}, [props.media, scrape]); |
||||
|
||||
if (loading) return <p>Scraping... ({percentage}%)</p>; |
||||
if (error) return <p>Errored, check console</p>; |
||||
|
||||
return ( |
||||
<div> |
||||
<Title className="mb-8">Output data</Title> |
||||
<code> |
||||
<pre>{JSON.stringify(results, null, 2)}</pre> |
||||
</code> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function ProviderSelector(props: ProviderSelectorProps) { |
||||
const providers = getProviders(); |
||||
|
||||
return ( |
||||
<div className="flex flex-col space-y-4"> |
||||
<Title className="mb-8">Choose provider</Title> |
||||
{providers.map((v) => ( |
||||
<ArrowLink |
||||
key={v.id} |
||||
onClick={() => props.onSelect(v.id)} |
||||
direction="right" |
||||
linkText={v.displayName} |
||||
/> |
||||
))} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function ProviderTesterView() { |
||||
const [media, setMedia] = useState<DetailedMeta | null>(null); |
||||
const [providerId, setProviderId] = useState<string | null>(null); |
||||
|
||||
let content: ReactNode = null; |
||||
if (!providerId) { |
||||
content = <ProviderSelector onSelect={(id) => setProviderId(id)} />; |
||||
} else if (!media) { |
||||
content = <MediaSelector onSelect={(v) => setMedia(v)} />; |
||||
} else { |
||||
content = <MediaScraper id={providerId} media={media} />; |
||||
} |
||||
|
||||
return ( |
||||
<div className="py-48"> |
||||
<Navigation /> |
||||
<div className="mx-8 overflow-x-auto">{content}</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,111 @@
@@ -0,0 +1,111 @@
|
||||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; |
||||
import { DetailedMeta } from "@/backend/metadata/getmeta"; |
||||
import { MWMediaType } from "@/backend/metadata/types"; |
||||
import { Button } from "@/components/Button"; |
||||
import { Dropdown } from "@/components/Dropdown"; |
||||
import { Navigation } from "@/components/layout/Navigation"; |
||||
import { ThinContainer } from "@/components/layout/ThinContainer"; |
||||
import { MetaController } from "@/video/components/controllers/MetaController"; |
||||
import { SourceController } from "@/video/components/controllers/SourceController"; |
||||
import { VideoPlayer } from "@/video/components/VideoPlayer"; |
||||
import { useCallback, useState } from "react"; |
||||
import { Helmet } from "react-helmet"; |
||||
|
||||
interface VideoData { |
||||
streamUrl: string; |
||||
type: MWStreamType; |
||||
} |
||||
|
||||
const testData: VideoData = { |
||||
streamUrl: |
||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", |
||||
type: MWStreamType.MP4, |
||||
}; |
||||
const testMeta: DetailedMeta = { |
||||
imdbId: "", |
||||
tmdbId: "", |
||||
meta: { |
||||
id: "hello-world", |
||||
title: "Big Buck Bunny", |
||||
type: MWMediaType.MOVIE, |
||||
seasons: undefined, |
||||
year: "2000", |
||||
}, |
||||
}; |
||||
|
||||
export function VideoTesterView() { |
||||
const [video, setVideo] = useState<VideoData | null>(null); |
||||
const [videoType, setVideoType] = useState<MWStreamType>(MWStreamType.MP4); |
||||
const [url, setUrl] = useState(""); |
||||
|
||||
const playVideo = useCallback( |
||||
(streamUrl: string) => { |
||||
setVideo({ |
||||
streamUrl, |
||||
type: videoType, |
||||
}); |
||||
}, |
||||
[videoType] |
||||
); |
||||
|
||||
if (video) { |
||||
return ( |
||||
<div className="fixed top-0 left-0 h-[100dvh] w-screen"> |
||||
<Helmet> |
||||
<html data-full="true" /> |
||||
</Helmet> |
||||
<VideoPlayer includeSafeArea autoPlay onGoBack={() => setVideo(null)}> |
||||
<MetaController |
||||
data={{ |
||||
captions: [], |
||||
meta: testMeta, |
||||
}} |
||||
linkedCaptions={[]} |
||||
/> |
||||
<SourceController |
||||
source={video.streamUrl} |
||||
type={MWStreamType.MP4} |
||||
quality={MWStreamQuality.Q720P} |
||||
/> |
||||
</VideoPlayer> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className="py-64"> |
||||
<Navigation /> |
||||
<ThinContainer classNames="flex items-start flex-col space-y-4"> |
||||
<div className="w-48"> |
||||
<Dropdown |
||||
options={[ |
||||
{ id: MWStreamType.MP4, name: "Mp4" }, |
||||
{ id: MWStreamType.HLS, name: "hls/m3u8" }, |
||||
]} |
||||
selectedItem={{ id: videoType, name: videoType }} |
||||
setSelectedItem={(a) => setVideoType(a.id as MWStreamType)} |
||||
/> |
||||
</div> |
||||
<div className="mb-4 flex gap-4"> |
||||
<input |
||||
type="text" |
||||
placeholder="stream url here..." |
||||
value={url} |
||||
onChange={(e) => setUrl(e.target.value)} |
||||
/> |
||||
<Button onClick={() => playVideo(url)}>Play video</Button> |
||||
</div> |
||||
<Button |
||||
onClick={() => |
||||
setVideo({ |
||||
streamUrl: testData.streamUrl, |
||||
type: testData.type, |
||||
}) |
||||
} |
||||
> |
||||
Play default video |
||||
</Button> |
||||
</ThinContainer> |
||||
</div> |
||||
); |
||||
} |
@ -1,25 +1,25 @@
@@ -1,25 +1,25 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"lib": ["dom", "dom.iterable", "esnext"], |
||||
"allowJs": true, |
||||
"skipLibCheck": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"strict": true, |
||||
"forceConsistentCasingInFileNames": true, |
||||
"noFallthroughCasesInSwitch": true, |
||||
"module": "esnext", |
||||
"moduleResolution": "node", |
||||
"resolveJsonModule": true, |
||||
"isolatedModules": true, |
||||
"noEmit": true, |
||||
"jsx": "react-jsx", |
||||
"baseUrl": "./src", |
||||
"paths": { |
||||
"@/*": ["./*"] |
||||
}, |
||||
"types": ["vite/client"] |
||||
}, |
||||
"include": ["src"] |
||||
} |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"lib": ["dom", "dom.iterable", "esnext"], |
||||
"allowJs": true, |
||||
"skipLibCheck": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"strict": true, |
||||
"forceConsistentCasingInFileNames": true, |
||||
"noFallthroughCasesInSwitch": true, |
||||
"module": "esnext", |
||||
"moduleResolution": "node", |
||||
"resolveJsonModule": true, |
||||
"isolatedModules": true, |
||||
"noEmit": true, |
||||
"jsx": "react-jsx", |
||||
"baseUrl": "./src", |
||||
"paths": { |
||||
"@/*": ["./*"] |
||||
}, |
||||
"types": ["vite/client", "vite-plugin-pwa/client"] |
||||
}, |
||||
"include": ["src"] |
||||
} |
||||
|
Loading…
Reference in new issue