chore: Update translations
This commit is contained in:
parent
03a5820140
commit
aa7aca8823
@ -35,10 +35,12 @@ type VideoPlayerProps = {
|
|||||||
export default function LiveVideoPlayer({ title, stream, status, poster, link, ...props }: VideoPlayerProps) {
|
export default function LiveVideoPlayer({ title, stream, status, poster, link, ...props }: VideoPlayerProps) {
|
||||||
function innerPlayer() {
|
function innerPlayer() {
|
||||||
if (stream === "nip94") {
|
if (stream === "nip94") {
|
||||||
return <Nip94Player link={link} />
|
return <Nip94Player link={link} />;
|
||||||
}
|
}
|
||||||
{/* @ts-ignore Web Componenet */ }
|
{
|
||||||
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />
|
/* @ts-ignore Web Componenet */
|
||||||
|
}
|
||||||
|
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MediaController
|
<MediaController
|
||||||
|
@ -1,158 +1,165 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { EventKind, NostrLink, QueryLike, RequestBuilder, SystemInterface, parseNostrLink } from "@snort/system";
|
import { EventKind, NostrLink, QueryLike, RequestBuilder, SystemInterface, parseNostrLink } from "@snort/system";
|
||||||
import { useContext, useEffect, useRef } from "react"
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import mpegts from "mpegts.js";
|
import mpegts from "mpegts.js";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { findTag } from "@/utils";
|
import { findTag } from "@/utils";
|
||||||
|
|
||||||
interface MediaSegment {
|
interface MediaSegment {
|
||||||
created: number,
|
created: number;
|
||||||
sha256: string,
|
sha256: string;
|
||||||
url: string,
|
url: string;
|
||||||
duration: number,
|
duration: number;
|
||||||
loaded: boolean
|
loaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LoaderStatus {
|
enum LoaderStatus {
|
||||||
kIdle = 0,
|
kIdle = 0,
|
||||||
kConnecting = 1,
|
kConnecting = 1,
|
||||||
kBuffering = 2,
|
kBuffering = 2,
|
||||||
kError = 3,
|
kError = 3,
|
||||||
kComplete = 4
|
kComplete = 4,
|
||||||
}
|
}
|
||||||
class Nip94Loader implements mpegts.BaseLoader {
|
class Nip94Loader implements mpegts.BaseLoader {
|
||||||
#system!: SystemInterface
|
#system!: SystemInterface;
|
||||||
#query?: QueryLike
|
#query?: QueryLike;
|
||||||
#mediaChunks: Map<string, MediaSegment> = new Map();
|
#mediaChunks: Map<string, MediaSegment> = new Map();
|
||||||
#status: LoaderStatus = LoaderStatus.kIdle;
|
#status: LoaderStatus = LoaderStatus.kIdle;
|
||||||
#bytes = 0;
|
#bytes = 0;
|
||||||
|
|
||||||
constructor(_seekHandler: mpegts.SeekHandler, config: mpegts.Config) {
|
constructor(_seekHandler: mpegts.SeekHandler, config: mpegts.Config) {
|
||||||
if ("system" in config) {
|
if ("system" in config) {
|
||||||
this.#system = config.system as SystemInterface;
|
this.#system = config.system as SystemInterface;
|
||||||
} else {
|
} else {
|
||||||
throw "Invalid config, missing system"
|
throw "Invalid config, missing system";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
onContentLengthKnown: (contentLength: number) => void;
|
||||||
|
// @ts-expect-error
|
||||||
|
onURLRedirect: (redirectedURL: string) => void;
|
||||||
|
// @ts-expect-error
|
||||||
|
onDataArrival: (chunk: ArrayBuffer, byteStart: number, receivedLength?: number | undefined) => void;
|
||||||
|
// @ts-expect-error
|
||||||
|
onError: (errorType: mpegts.LoaderErrors, errorInfo: mpegts.LoaderErrorMessage) => void;
|
||||||
|
// @ts-expect-error
|
||||||
|
onComplete: (rangeFrom: number, rangeTo: number) => void;
|
||||||
|
|
||||||
|
get _status() {
|
||||||
|
return this.#status;
|
||||||
|
}
|
||||||
|
get status() {
|
||||||
|
return this.#status;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
isWorking(): boolean {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "nip94";
|
||||||
|
get needStashBuffer() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get _needStash() {
|
||||||
|
return this.needStashBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(dataSource: mpegts.MediaSegment, range: mpegts.Range) {
|
||||||
|
const link = parseNostrLink(dataSource.url);
|
||||||
|
if (!link) {
|
||||||
|
throw new Error("Datasource.url is invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error
|
const rb = new RequestBuilder(`n94-stream-${link.encode()}`);
|
||||||
onContentLengthKnown: (contentLength: number) => void;
|
rb.withOptions({
|
||||||
// @ts-expect-error
|
leaveOpen: true,
|
||||||
onURLRedirect: (redirectedURL: string) => void;
|
});
|
||||||
// @ts-expect-error
|
rb.withFilter().replyToLink([link]).kinds([EventKind.FileHeader]);
|
||||||
onDataArrival: (chunk: ArrayBuffer, byteStart: number, receivedLength?: number | undefined) => void;
|
|
||||||
// @ts-expect-error
|
|
||||||
onError: (errorType: mpegts.LoaderErrors, errorInfo: mpegts.LoaderErrorMessage) => void;
|
|
||||||
// @ts-expect-error
|
|
||||||
onComplete: (rangeFrom: number, rangeTo: number) => void;
|
|
||||||
|
|
||||||
get _status() { return this.#status }
|
this.#status = LoaderStatus.kConnecting;
|
||||||
get status() { return this.#status }
|
this.#query = this.#system.Query(rb);
|
||||||
|
this.#query.on("event", evs => {
|
||||||
|
for (const ev of evs) {
|
||||||
|
const seg = {
|
||||||
|
created: ev.created_at,
|
||||||
|
url: findTag(ev, "url")!,
|
||||||
|
sha256: findTag(ev, "x")!,
|
||||||
|
} as MediaSegment;
|
||||||
|
this.#addSegment(seg);
|
||||||
|
}
|
||||||
|
this.#loadNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
destroy(): void {
|
abort() {
|
||||||
throw new Error("Method not implemented.");
|
this.#query?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
#addSegment(seg: MediaSegment) {
|
||||||
|
if (!this.#mediaChunks.has(seg.sha256)) {
|
||||||
|
this.#mediaChunks.set(seg.sha256, seg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isWorking(): boolean {
|
async #loadNext() {
|
||||||
throw new Error("Method not implemented.");
|
if (this.#status === LoaderStatus.kConnecting || this.#status === LoaderStatus.kIdle) {
|
||||||
|
this.#status = LoaderStatus.kBuffering;
|
||||||
}
|
}
|
||||||
|
if (this.#status !== LoaderStatus.kBuffering) {
|
||||||
type = "nip94"
|
return;
|
||||||
get needStashBuffer() {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
get _needStash() { return this.needStashBuffer }
|
const orderedLoad = [...this.#mediaChunks.values()].sort((a, b) => a.created - b.created).filter(a => !a.loaded);
|
||||||
|
for (const s of orderedLoad) {
|
||||||
|
const result = await fetch(s.url);
|
||||||
|
const buf = await result.arrayBuffer();
|
||||||
|
|
||||||
open(dataSource: mpegts.MediaSegment, range: mpegts.Range) {
|
this.onDataArrival(buf, this.#bytes, buf.byteLength);
|
||||||
const link = parseNostrLink(dataSource.url);
|
console.debug("pushing bytes", this.#bytes, buf.byteLength);
|
||||||
if (!link) {
|
this.#bytes += buf.byteLength;
|
||||||
throw new Error("Datasource.url is invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
const rb = new RequestBuilder(`n94-stream-${link.encode()}`);
|
this.#mediaChunks.set(s.sha256, {
|
||||||
rb.withOptions({
|
...s,
|
||||||
leaveOpen: true,
|
loaded: true,
|
||||||
});
|
});
|
||||||
rb.withFilter()
|
|
||||||
.replyToLink([link])
|
|
||||||
.kinds([EventKind.FileHeader]);
|
|
||||||
|
|
||||||
this.#status = LoaderStatus.kConnecting;
|
|
||||||
this.#query = this.#system.Query(rb);
|
|
||||||
this.#query.on("event", (evs) => {
|
|
||||||
for (const ev of evs) {
|
|
||||||
const seg = {
|
|
||||||
created: ev.created_at,
|
|
||||||
url: findTag(ev, "url")!,
|
|
||||||
sha256: findTag(ev, "x")!,
|
|
||||||
} as MediaSegment;
|
|
||||||
this.#addSegment(seg);
|
|
||||||
}
|
|
||||||
this.#loadNext();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
abort() {
|
|
||||||
this.#query?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
#addSegment(seg: MediaSegment) {
|
|
||||||
if (!this.#mediaChunks.has(seg.sha256)) {
|
|
||||||
this.#mediaChunks.set(seg.sha256, seg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #loadNext() {
|
|
||||||
if (this.#status === LoaderStatus.kConnecting || this.#status === LoaderStatus.kIdle) {
|
|
||||||
this.#status = LoaderStatus.kBuffering;
|
|
||||||
}
|
|
||||||
if (this.#status !== LoaderStatus.kBuffering) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const orderedLoad = [...this.#mediaChunks.values()].sort((a, b) => a.created - b.created).filter(a => !a.loaded);
|
|
||||||
for (const s of orderedLoad) {
|
|
||||||
const result = await fetch(s.url);
|
|
||||||
const buf = await result.arrayBuffer();
|
|
||||||
|
|
||||||
this.onDataArrival(buf, this.#bytes, buf.byteLength);
|
|
||||||
console.debug("pushing bytes", this.#bytes, buf.byteLength);
|
|
||||||
this.#bytes += buf.byteLength;
|
|
||||||
|
|
||||||
this.#mediaChunks.set(s.sha256, {
|
|
||||||
...s,
|
|
||||||
loaded: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.#status = LoaderStatus.kIdle;
|
|
||||||
}
|
}
|
||||||
|
this.#status = LoaderStatus.kIdle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Nip94Player({ link }: { link: NostrLink }) {
|
export default function Nip94Player({ link }: { link: NostrLink }) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const player = ref.current as HTMLVideoElement & {
|
const player = ref.current as HTMLVideoElement & {
|
||||||
__streamer?: mpegts.Player
|
__streamer?: mpegts.Player;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!player.__streamer) {
|
if (!player.__streamer) {
|
||||||
player.__streamer = new mpegts.MSEPlayer({
|
player.__streamer = new mpegts.MSEPlayer(
|
||||||
type: "mse",
|
{
|
||||||
isLive: true,
|
type: "mse",
|
||||||
url: link.encode()
|
isLive: true,
|
||||||
}, {
|
url: link.encode(),
|
||||||
system,
|
},
|
||||||
customLoader: Nip94Loader
|
{
|
||||||
} as unknown as mpegts.Config);
|
system,
|
||||||
|
customLoader: Nip94Loader,
|
||||||
|
} as unknown as mpegts.Config,
|
||||||
|
);
|
||||||
|
|
||||||
player.__streamer.attachMediaElement(player);
|
player.__streamer.attachMediaElement(player);
|
||||||
player.__streamer.load();
|
player.__streamer.load();
|
||||||
player.__streamer.play();
|
player.__streamer.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
return <video slot="media" ref={ref}></video>
|
return <video slot="media" ref={ref}></video>;
|
||||||
}
|
}
|
||||||
|
@ -36,9 +36,7 @@ export function useSortedStreams(feed: Array<TaggedNostrEvent>, oldest?: number)
|
|||||||
const live = feedSorted
|
const live = feedSorted
|
||||||
.filter(a => {
|
.filter(a => {
|
||||||
try {
|
try {
|
||||||
return (
|
return findTag(a, "status") === StreamState.Live && canPlayEvent(a);
|
||||||
findTag(a, "status") === StreamState.Live && canPlayEvent(a)
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
|
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{ev?.kind === LIVE_STREAM &&
|
{ev?.kind === LIVE_STREAM && (
|
||||||
<LiveVideoPlayer
|
<LiveVideoPlayer
|
||||||
title={title}
|
title={title}
|
||||||
stream={status === StreamState.Live ? stream : recording}
|
stream={status === StreamState.Live ? stream : recording}
|
||||||
@ -64,7 +64,8 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
|
|||||||
status={status}
|
status={status}
|
||||||
link={evLink}
|
link={evLink}
|
||||||
className="max-xl:max-h-[30vh] xl:w-full xl:max-h-[85dvh] mx-auto"
|
className="max-xl:max-h-[30vh] xl:w-full xl:max-h-[85dvh] mx-auto"
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<div className="lg:px-5 max-lg:px-2">
|
<div className="lg:px-5 max-lg:px-2">
|
||||||
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
|
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
|
||||||
|
@ -115,7 +115,7 @@ export interface StreamInfo {
|
|||||||
host?: string;
|
host?: string;
|
||||||
gameId?: string;
|
gameId?: string;
|
||||||
gameInfo?: GameInfo;
|
gameInfo?: GameInfo;
|
||||||
streams: Array<string>
|
streams: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameTagFormat = /^[a-z-]+:[a-z0-9-]+$/i;
|
const gameTagFormat = /^[a-z-]+:[a-z0-9-]+$/i;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user