Skip to content

Commit a128ee7

Browse files
authored
lvpr-tv: postMessage & controls (#615)
* lvpr-tv: postMessage & controls * remove generic log * remove unused * iframe cors * x-frame header for firefox
1 parent 5dadfc8 commit a128ee7

File tree

5 files changed

+387
-5
lines changed

5 files changed

+387
-5
lines changed

apps/lvpr-tv/next.config.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
reactStrictMode: true,
4+
async headers() {
5+
return [
6+
{
7+
source: "/(.*)",
8+
headers: [
9+
{
10+
key: "Content-Security-Policy",
11+
value:
12+
"frame-ancestors 'self' https://vtuber.fun https://dev.vtuber.fun https://daydream.live https://*.livepeer.monster https://*.livepeer.org https://*.vercel.app http://localhost:3000",
13+
},
14+
{
15+
key: "X-Frame-Options",
16+
value:
17+
"ALLOW-FROM https://vtuber.fun https://dev.vtuber.fun https://daydream.live https://*.livepeer.monster https://*.livepeer.org https://*.vercel.app http://localhost:3000",
18+
},
19+
{
20+
key: "Access-Control-Allow-Origin",
21+
value:
22+
"https://vtuber.fun https://dev.vtuber.fun https://daydream.live https://*.livepeer.monster https://*.livepeer.org https://*.vercel.app http://localhost:3000",
23+
},
24+
{
25+
key: "Access-Control-Allow-Methods",
26+
value: "GET, POST, PUT, DELETE, OPTIONS",
27+
},
28+
{
29+
key: "Access-Control-Allow-Headers",
30+
value: "Content-Type, Authorization",
31+
},
32+
],
33+
},
34+
];
35+
},
436
};
537

638
module.exports = nextConfig;

apps/lvpr-tv/src/app/page.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {
22
PlayerLoading,
33
type PlayerProps,
44
PlayerWithControls,
5+
PlayerWithoutControls,
56
} from "@/components/player/Player";
67
import type { Booleanish } from "@/lib/types";
78
import { coerceToBoolean } from "@/lib/utils";
89
import type { ClipLength } from "@livepeer/react";
910
import { Suspense } from "react";
11+
import { IframeMessenger } from "../components/IframeMessenger";
1012

1113
type Autoplay = Booleanish;
1214
type Muted = Booleanish;
@@ -16,6 +18,7 @@ type ObjectFit = "contain" | "cover";
1618
type Constant = Booleanish;
1719
type Debug = Booleanish;
1820
type IngestPlayback = Booleanish;
21+
type Controls = Booleanish;
1922

2023
type PlayerSearchParams = {
2124
v?: string;
@@ -33,6 +36,7 @@ type PlayerSearchParams = {
3336
accessKey?: string;
3437
debug?: Debug;
3538
ingestPlayback?: IngestPlayback;
39+
controls?: Controls;
3640
};
3741

3842
export default async function PlayerPage({
@@ -63,14 +67,24 @@ export default async function PlayerPage({
6367
accessKey: searchParams?.accessKey ?? null,
6468
debug: coerceToBoolean(searchParams?.debug, false),
6569
ingestPlayback: coerceToBoolean(searchParams?.ingestPlayback, false),
70+
controls: coerceToBoolean(searchParams?.controls, true),
6671
};
6772

73+
const showControls = coerceToBoolean(searchParams?.controls, true);
74+
6875
return (
69-
<main className="absolute flex flex-col justify-center items-center h-full w-full inset-0 bg-black">
70-
<Suspense fallback={<PlayerLoading />}>
71-
<PlayerWithControls {...props} />
72-
</Suspense>
73-
</main>
76+
<>
77+
<IframeMessenger />
78+
<main className="absolute flex flex-col justify-center items-center h-full w-full inset-0 bg-black">
79+
<Suspense fallback={<PlayerLoading />}>
80+
{showControls ? (
81+
<PlayerWithControls {...props} />
82+
) : (
83+
<PlayerWithoutControls {...props} />
84+
)}
85+
</Suspense>
86+
</main>
87+
</>
7488
);
7589
}
7690

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
interface PlayerError {
5+
message: string;
6+
code?: string;
7+
details?: unknown;
8+
timestamp: number;
9+
}
10+
11+
function isAllowedOrigin(origin: string): boolean {
12+
try {
13+
const url = new URL(origin);
14+
const hostname = url.hostname;
15+
const port = url.port;
16+
17+
if (
18+
hostname === "daydream.live" ||
19+
hostname === "vtuber.fun" ||
20+
hostname === "dev.vtuber.fun" ||
21+
(hostname === "localhost" && port === "3000")
22+
) {
23+
return true;
24+
}
25+
26+
if (
27+
hostname.endsWith(".livepeer.monster") ||
28+
hostname.endsWith(".livepeer.org") ||
29+
hostname.endsWith(".vercel.app")
30+
) {
31+
return true;
32+
}
33+
34+
return false;
35+
} catch {
36+
return false;
37+
}
38+
}
39+
40+
function isInIframe(): boolean {
41+
try {
42+
return typeof window !== "undefined" && window.self !== window.top;
43+
} catch (e) {
44+
return true;
45+
}
46+
}
47+
48+
export function IframeMessenger() {
49+
useEffect(() => {
50+
if (!isInIframe()) {
51+
return;
52+
}
53+
54+
const handleMessage = (event: MessageEvent) => {
55+
if (!isAllowedOrigin(event.origin)) {
56+
return;
57+
}
58+
59+
//console.log("Received message from parent:", event.data);
60+
};
61+
62+
const sendReady = () => {
63+
if (window.parent && window.parent !== window) {
64+
window.parent.postMessage(
65+
{
66+
type: "lvpr-player-ready",
67+
timestamp: Date.now(),
68+
},
69+
"*",
70+
);
71+
}
72+
};
73+
74+
const handlePlayerError = (error: ErrorEvent | PromiseRejectionEvent) => {
75+
if (window.parent && window.parent !== window) {
76+
const playerError: PlayerError = {
77+
message: "message" in error ? error.message : "Unknown error",
78+
timestamp: Date.now(),
79+
};
80+
81+
if ("filename" in error) {
82+
playerError.details = {
83+
filename: error.filename,
84+
lineno: error.lineno,
85+
colno: error.colno,
86+
};
87+
}
88+
89+
if ("reason" in error) {
90+
playerError.message =
91+
error.reason?.message || "Unhandled promise rejection";
92+
playerError.details = error.reason;
93+
}
94+
95+
window.parent.postMessage(
96+
{
97+
type: "lvpr-player-error",
98+
error: playerError,
99+
},
100+
"*",
101+
);
102+
}
103+
};
104+
105+
const handlePlayerEvent = (event: CustomEvent) => {
106+
if (window.parent && window.parent !== window) {
107+
window.parent.postMessage(
108+
{
109+
type: "lvpr-player-event",
110+
eventType: event.type,
111+
data: event.detail,
112+
timestamp: Date.now(),
113+
},
114+
"*",
115+
);
116+
}
117+
};
118+
119+
window.addEventListener("message", handleMessage);
120+
121+
window.addEventListener("error", handlePlayerError);
122+
123+
window.addEventListener("unhandledrejection", handlePlayerError);
124+
125+
window.addEventListener(
126+
"lvpr-player-error",
127+
handlePlayerEvent as EventListener,
128+
);
129+
window.addEventListener(
130+
"lvpr-player-offline",
131+
handlePlayerEvent as EventListener,
132+
);
133+
window.addEventListener(
134+
"lvpr-player-access-control",
135+
handlePlayerEvent as EventListener,
136+
);
137+
138+
sendReady();
139+
140+
return () => {
141+
window.removeEventListener("message", handleMessage);
142+
window.removeEventListener("error", handlePlayerError);
143+
window.removeEventListener("unhandledrejection", handlePlayerError);
144+
window.removeEventListener(
145+
"lvpr-player-error",
146+
handlePlayerEvent as EventListener,
147+
);
148+
window.removeEventListener(
149+
"lvpr-player-offline",
150+
handlePlayerEvent as EventListener,
151+
);
152+
window.removeEventListener(
153+
"lvpr-player-access-control",
154+
handlePlayerEvent as EventListener,
155+
);
156+
};
157+
}, []);
158+
159+
return null;
160+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
3+
import {
4+
type MediaScopedProps,
5+
useMediaContext,
6+
useStore,
7+
} from "@livepeer/react/player";
8+
import { useEffect, useRef } from "react";
9+
10+
interface PlayerEventDetail {
11+
type: string;
12+
message: string;
13+
error?: unknown;
14+
}
15+
16+
function dispatchPlayerEvent(type: string, detail?: PlayerEventDetail) {
17+
if (typeof window !== "undefined") {
18+
window.dispatchEvent(new CustomEvent(type, { detail }));
19+
}
20+
}
21+
22+
export function PlayerErrorMonitor({
23+
__scopeMedia,
24+
}: MediaScopedProps<Record<string, never>>) {
25+
const context = useMediaContext("PlayerErrorMonitor", __scopeMedia);
26+
const previousError = useRef<unknown>(null);
27+
28+
const { error } = useStore(context.store, ({ error }) => ({
29+
error,
30+
}));
31+
32+
useEffect(() => {
33+
if (error && error !== previousError.current) {
34+
previousError.current = error;
35+
36+
let errorType = "general";
37+
let message = "Player error occurred";
38+
39+
const errorObj = error as { type?: string };
40+
41+
if (errorObj.type === "offline") {
42+
errorType = "offline";
43+
message = "Stream is offline";
44+
dispatchPlayerEvent("lvpr-player-offline", {
45+
type: errorType,
46+
message,
47+
error,
48+
});
49+
} else if (errorObj.type === "access-control") {
50+
errorType = "access-control";
51+
message = "Stream is private - access denied";
52+
dispatchPlayerEvent("lvpr-player-access-control", {
53+
type: errorType,
54+
message,
55+
error,
56+
});
57+
} else {
58+
dispatchPlayerEvent("lvpr-player-error", {
59+
type: errorType,
60+
message,
61+
error,
62+
});
63+
}
64+
}
65+
}, [error]);
66+
67+
return null;
68+
}

0 commit comments

Comments
 (0)