Skip to content

Commit ed0f0d0

Browse files
committed
chore: refactor ipfs video player into mod
1 parent 6b6d9f4 commit ed0f0d0

File tree

8 files changed

+171
-45
lines changed

8 files changed

+171
-45
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
export async function POST(request: NextRequest) {
4+
const form = await request.formData();
5+
6+
const controller = new AbortController();
7+
const signal = controller.signal;
8+
9+
// Cancel upload if it takes longer than 15s
10+
setTimeout(() => {
11+
controller.abort();
12+
}, 15_000);
13+
14+
const uploadRes: Response | null = await fetch(
15+
"https://ipfs.infura.io:5001/api/v0/add",
16+
{
17+
method: "POST",
18+
body: form,
19+
headers: {
20+
Authorization:
21+
"Basic " +
22+
Buffer.from(
23+
process.env.INFURA_API_KEY + ":" + process.env.INFURA_API_SECRET
24+
).toString("base64"),
25+
},
26+
signal,
27+
}
28+
);
29+
30+
const { Hash: hash } = await uploadRes.json();
31+
32+
const responseData = { url: `ipfs://${hash}` };
33+
34+
return NextResponse.json({ data: responseData });
35+
}
36+
37+
// needed for preflight requests to succeed
38+
export const OPTIONS = async (request: NextRequest) => {
39+
return NextResponse.json({});
40+
};
41+
42+
export const GET = async (
43+
req: NextRequest,
44+
{ params }: { params: { assetId: string } }
45+
) => {
46+
const assetRequest = await fetch(
47+
`https://livepeer.studio/api/asset/${params.assetId}`,
48+
{
49+
method: "GET",
50+
headers: {
51+
Authorization: `Bearer ${process.env.LIVEPEER_API_SECRET}`,
52+
},
53+
}
54+
);
55+
56+
const assetResponseJson = await assetRequest.json();
57+
const { playbackUrl } = assetResponseJson;
58+
59+
if (!playbackUrl) {
60+
return NextResponse.json({}, { status: 404 });
61+
}
62+
63+
return NextResponse.json({
64+
url: playbackUrl,
65+
});
66+
};
67+
68+
export const runtime = "edge";

examples/api/src/app/api/livepeer-video/route.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,8 @@ export const GET = async (request: NextRequest) => {
7777

7878
const { asset } = await uploadRes.json();
7979

80-
const playbackUrl = `https://lp-playback.com/hls/${asset.playbackId}/index.m3u8`;
81-
8280
return NextResponse.json({
83-
url: playbackUrl,
81+
id: asset.id,
8482
fallbackUrl: gatewayUrl,
8583
mimeType: contentType,
8684
});

examples/api/src/app/api/open-graph/lib/url-handlers/ipfs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ async function handleIpfsUrl(url: string): Promise<UrlMetadata | null> {
3030
}
3131

3232
const handler: UrlHandler = {
33+
name: "IPFS",
3334
matchers: ["ipfs://.*"],
3435
handler: handleIpfsUrl,
3536
};

examples/nextjs-shadcn/src/app/dummy-casts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ export const dummyCastData: Array<{
113113
embeds: [
114114
// video embed
115115
{
116-
url: "https://lp-playback.com/hls/3087gff2uxc09ze1/index.m3u8",
117-
// url: "ipfs://QmdeTAKogKpZVLpp2JLsjfM83QV46bnVrHTP1y89DvR57i",
116+
// url: "https://lp-playback.com/hls/3087gff2uxc09ze1/index.m3u8",
117+
url: "ipfs://QmdeTAKogKpZVLpp2JLsjfM83QV46bnVrHTP1y89DvR57i",
118118
status: "loaded",
119119
metadata: {
120120
mimeType: "video/mp4",

mods/video-render/src/view.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,67 @@ import { ModElement } from "@mod-protocol/core";
22

33
const view: ModElement[] = [
44
{
5-
type: "video",
6-
videoSrc: "{{embed.url}}",
5+
type: "vertical-layout",
6+
elements: [
7+
{
8+
if: {
9+
value: "{{embed.url}}",
10+
match: {
11+
startsWith: "ipfs://",
12+
},
13+
},
14+
then: {
15+
type: "vertical-layout",
16+
elements: [
17+
{
18+
if: {
19+
value: "{{refs.transcodedResponse.response.data.url}}",
20+
match: {
21+
NOT: {
22+
equals: "",
23+
},
24+
},
25+
},
26+
then: {
27+
type: "video",
28+
videoSrc: "{{refs.transcodedResponse.response.data.url}}",
29+
// .m3u8
30+
mimeType: "application/x-mpegURL",
31+
},
32+
else: {
33+
type: "vertical-layout",
34+
elements: [
35+
{
36+
type: "video",
37+
videoSrc:
38+
"https://cloudflare-ipfs.com/ipfs/{{embed.url | split ipfs:// | index 1}}",
39+
mimeType: "{{embed.metadata.mimeType}}",
40+
},
41+
{
42+
type: "button",
43+
label: "Load stream",
44+
onclick: {
45+
type: "GET",
46+
url: "{{api}}/livepeer-video",
47+
searchParams: {
48+
url: "{{embed.url}}",
49+
},
50+
ref: "transcodingResponse",
51+
onsuccess: {
52+
type: "GET",
53+
url: "{{api}}/livepeer-video/{{refs.transcodingResponse.response.data.id}}",
54+
ref: "transcodedResponse",
55+
retryTimeout: 1000,
56+
},
57+
},
58+
},
59+
],
60+
},
61+
},
62+
],
63+
},
64+
},
65+
],
766
},
867
];
968

packages/core/src/manifest.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,14 @@ type HTTPBody =
8989
formData: Record<string, FormDataType>;
9090
};
9191

92-
export type HTTPAction = BaseAction & { url: string } & (
92+
export type HTTPAction = BaseAction & {
93+
url: string;
94+
retryTimeout?: number;
95+
retryCount?: number;
96+
} & (
97+
| {
98+
type: "HEAD";
99+
}
93100
| {
94101
type: "GET";
95102
searchParams?: Record<string, string>;
@@ -240,6 +247,7 @@ export type ModElement =
240247
| {
241248
type: "video";
242249
videoSrc: string;
250+
mimeType?: string;
243251
}
244252
| {
245253
type: "tabs";

packages/core/src/renderer.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type ModElementRef<T> =
3131
| {
3232
type: "video";
3333
videoSrc: string;
34+
mimeType?: string;
3435
}
3536
| {
3637
type: "link";
@@ -628,6 +629,7 @@ export class Renderer {
628629
case "POST":
629630
case "PUT":
630631
case "PATCH":
632+
case "HEAD":
631633
case "DELETE": {
632634
const options = this.constructHttpAction(action);
633635

@@ -677,8 +679,26 @@ export class Renderer {
677679
}
678680

679681
if (action.ref) {
680-
set(this.refs, action.ref, { error });
682+
const actionRef = get(this.refs, action.ref);
683+
const retries = actionRef?._retries || 0;
684+
set(this.refs, action.ref, {
685+
...actionRef,
686+
error,
687+
_retries: retries + 1,
688+
});
681689
this.onTreeChange();
690+
691+
if (action.retryTimeout) {
692+
if (
693+
action.retryCount !== undefined
694+
? retries < action.retryCount
695+
: true
696+
) {
697+
setTimeout(() => {
698+
this.stepIntoOrTriggerAction(action);
699+
}, action.retryTimeout);
700+
}
701+
}
682702
}
683703

684704
this.asyncAction = null;
@@ -1213,6 +1233,9 @@ export class Renderer {
12131233
{
12141234
type: "video",
12151235
videoSrc: this.replaceInlineContext(el.videoSrc),
1236+
mimeType: el.mimeType
1237+
? this.replaceInlineContext(el.mimeType)
1238+
: undefined,
12161239
},
12171240
key
12181241
);

packages/react-ui-shadcn/src/renderers/video.tsx

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import videojs from "video.js";
55

66
interface PlayerProps {
77
videoSrc: string;
8+
mimeType?: string;
89
}
910

1011
const videoJSoptions: {
@@ -31,28 +32,10 @@ export const VideoRenderer = (props: PlayerProps) => {
3132
const playerRef = React.useRef<any>(null);
3233

3334
const [videoSrc, setVideoSrc] = React.useState<string | undefined>();
34-
const [overrideMimeType, setOverrideMimeType] = React.useState<
35-
string | undefined
36-
>(undefined);
3735

3836
const [hasStartedPlaying, setHasStartedPlaying] =
3937
React.useState<boolean>(false);
4038

41-
const pollUrl = useCallback(
42-
async (url: string) => {
43-
const res = await fetch(url, { method: "HEAD" });
44-
if (hasStartedPlaying) return;
45-
if (res.ok) {
46-
setVideoSrc(url);
47-
} else {
48-
setTimeout(() => {
49-
pollUrl(url);
50-
}, 1000);
51-
}
52-
},
53-
[setVideoSrc, hasStartedPlaying]
54-
);
55-
5639
const options = useMemo(
5740
() => ({
5841
...videoJSoptions,
@@ -61,7 +44,7 @@ export const VideoRenderer = (props: PlayerProps) => {
6144
{
6245
src: videoSrc ?? "",
6346
type:
64-
overrideMimeType ||
47+
props.mimeType ||
6548
(videoSrc?.endsWith(".m3u8")
6649
? "application/x-mpegURL"
6750
: videoSrc?.endsWith(".mp4")
@@ -70,26 +53,12 @@ export const VideoRenderer = (props: PlayerProps) => {
7053
},
7154
],
7255
}),
73-
[videoSrc, overrideMimeType]
56+
[videoSrc, props.mimeType]
7457
);
7558

7659
useEffect(() => {
77-
if (props.videoSrc.startsWith("ipfs://")) {
78-
// Exchange ipfs:// for .m3u8 url via /livepeer-video?url=ipfs://...
79-
const baseUrl = `${
80-
process.env.NEXT_PUBLIC_API_URL || "https://api.modprotocol.org"
81-
}/livepeer-video`;
82-
const endpointUrl = `${baseUrl}?url=${props.videoSrc}`;
83-
fetch(endpointUrl).then(async (res) => {
84-
const { url, fallbackUrl, mimeType } = await res.json();
85-
setOverrideMimeType(mimeType);
86-
setVideoSrc(`${fallbackUrl}`);
87-
pollUrl(url);
88-
});
89-
} else {
90-
setVideoSrc(props.videoSrc);
91-
}
92-
}, [props.videoSrc, pollUrl]);
60+
setVideoSrc(props.videoSrc);
61+
}, [props.videoSrc]);
9362

9463
useEffect(() => {
9564
// Make sure Video.js player is only initialized once

0 commit comments

Comments
 (0)