Skip to content

Commit 2071ec1

Browse files
committed
chore: refactor ipfs video player into mod
1 parent 613fb4b commit 2071ec1

File tree

8 files changed

+173
-45
lines changed

8 files changed

+173
-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: 26 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";
@@ -626,6 +627,7 @@ export class Renderer {
626627
case "POST":
627628
case "PUT":
628629
case "PATCH":
630+
case "HEAD":
629631
case "DELETE": {
630632
const options = this.constructHttpAction(action);
631633

@@ -654,6 +656,7 @@ export class Renderer {
654656

655657
if (action.ref) {
656658
set(this.refs, action.ref, { response, progress: 100 });
659+
this.onTreeChange();
657660
}
658661

659662
if (action.onsuccess) {
@@ -674,7 +677,26 @@ export class Renderer {
674677
}
675678

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

680702
this.asyncAction = null;
@@ -1203,6 +1225,9 @@ export class Renderer {
12031225
{
12041226
type: "video",
12051227
videoSrc: this.replaceInlineContext(el.videoSrc),
1228+
mimeType: el.mimeType
1229+
? this.replaceInlineContext(el.mimeType)
1230+
: undefined,
12061231
},
12071232
key
12081233
);

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

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "video.js/dist/video-js.css";
66

77
interface PlayerProps {
88
videoSrc: string;
9+
mimeType?: string;
910
}
1011

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

3435
const [videoSrc, setVideoSrc] = React.useState<string | undefined>();
35-
const [overrideMimeType, setOverrideMimeType] = React.useState<
36-
string | undefined
37-
>(undefined);
3836

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

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

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

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

0 commit comments

Comments
 (0)