Skip to content

Commit

Permalink
chore: refactor ipfs video player into mod
Browse files Browse the repository at this point in the history
  • Loading branch information
stephancill committed Dec 8, 2023
1 parent 613fb4b commit 2071ec1
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 45 deletions.
68 changes: 68 additions & 0 deletions examples/api/src/app/api/livepeer-video/[assetId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
const form = await request.formData();

const controller = new AbortController();
const signal = controller.signal;

// Cancel upload if it takes longer than 15s
setTimeout(() => {
controller.abort();
}, 15_000);

const uploadRes: Response | null = await fetch(
"https://ipfs.infura.io:5001/api/v0/add",
{
method: "POST",
body: form,
headers: {
Authorization:
"Basic " +
Buffer.from(
process.env.INFURA_API_KEY + ":" + process.env.INFURA_API_SECRET
).toString("base64"),
},
signal,
}
);

const { Hash: hash } = await uploadRes.json();

const responseData = { url: `ipfs://${hash}` };

return NextResponse.json({ data: responseData });
}

// needed for preflight requests to succeed
export const OPTIONS = async (request: NextRequest) => {
return NextResponse.json({});
};

export const GET = async (
req: NextRequest,
{ params }: { params: { assetId: string } }
) => {
const assetRequest = await fetch(
`https://livepeer.studio/api/asset/${params.assetId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${process.env.LIVEPEER_API_SECRET}`,
},
}
);

const assetResponseJson = await assetRequest.json();
const { playbackUrl } = assetResponseJson;

if (!playbackUrl) {
return NextResponse.json({}, { status: 404 });
}

return NextResponse.json({
url: playbackUrl,
});
};

export const runtime = "edge";
4 changes: 1 addition & 3 deletions examples/api/src/app/api/livepeer-video/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,8 @@ export const GET = async (request: NextRequest) => {

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

const playbackUrl = `https://lp-playback.com/hls/${asset.playbackId}/index.m3u8`;

return NextResponse.json({
url: playbackUrl,
id: asset.id,
fallbackUrl: gatewayUrl,
mimeType: contentType,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async function handleIpfsUrl(url: string): Promise<UrlMetadata | null> {
}

const handler: UrlHandler = {
name: "IPFS",
matchers: ["ipfs://.*"],
handler: handleIpfsUrl,
};
Expand Down
4 changes: 2 additions & 2 deletions examples/nextjs-shadcn/src/app/dummy-casts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ export const dummyCastData: Array<{
embeds: [
// video embed
{
url: "https://lp-playback.com/hls/3087gff2uxc09ze1/index.m3u8",
// url: "ipfs://QmdeTAKogKpZVLpp2JLsjfM83QV46bnVrHTP1y89DvR57i",
// url: "https://lp-playback.com/hls/3087gff2uxc09ze1/index.m3u8",
url: "ipfs://QmdeTAKogKpZVLpp2JLsjfM83QV46bnVrHTP1y89DvR57i",
status: "loaded",
metadata: {
mimeType: "video/mp4",
Expand Down
63 changes: 61 additions & 2 deletions mods/video-render/src/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,67 @@ import { ModElement } from "@mod-protocol/core";

const view: ModElement[] = [
{
type: "video",
videoSrc: "{{embed.url}}",
type: "vertical-layout",
elements: [
{
if: {
value: "{{embed.url}}",
match: {
startsWith: "ipfs://",
},
},
then: {
type: "vertical-layout",
elements: [
{
if: {
value: "{{refs.transcodedResponse.response.data.url}}",
match: {
NOT: {
equals: "",
},
},
},
then: {
type: "video",
videoSrc: "{{refs.transcodedResponse.response.data.url}}",
// .m3u8
mimeType: "application/x-mpegURL",
},
else: {
type: "vertical-layout",
elements: [
{
type: "video",
videoSrc:
"https://cloudflare-ipfs.com/ipfs/{{embed.url | split ipfs:// | index 1}}",
mimeType: "{{embed.metadata.mimeType}}",
},
{
type: "button",
label: "Load stream",
onclick: {
type: "GET",
url: "{{api}}/livepeer-video",
searchParams: {
url: "{{embed.url}}",
},
ref: "transcodingResponse",
onsuccess: {
type: "GET",
url: "{{api}}/livepeer-video/{{refs.transcodingResponse.response.data.id}}",
ref: "transcodedResponse",
retryTimeout: 1000,
},
},
},
],
},
},
],
},
},
],
},
];

Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ type HTTPBody =
formData: Record<string, FormDataType>;
};

export type HTTPAction = BaseAction & { url: string } & (
export type HTTPAction = BaseAction & {
url: string;
retryTimeout?: number;
retryCount?: number;
} & (
| {
type: "HEAD";
}
| {
type: "GET";
searchParams?: Record<string, string>;
Expand Down Expand Up @@ -240,6 +247,7 @@ export type ModElement =
| {
type: "video";
videoSrc: string;
mimeType?: string;
}
| {
type: "tabs";
Expand Down
27 changes: 26 additions & 1 deletion packages/core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type ModElementRef<T> =
| {
type: "video";
videoSrc: string;
mimeType?: string;
}
| {
type: "link";
Expand Down Expand Up @@ -626,6 +627,7 @@ export class Renderer {
case "POST":
case "PUT":
case "PATCH":
case "HEAD":
case "DELETE": {
const options = this.constructHttpAction(action);

Expand Down Expand Up @@ -654,6 +656,7 @@ export class Renderer {

if (action.ref) {
set(this.refs, action.ref, { response, progress: 100 });
this.onTreeChange();
}

if (action.onsuccess) {
Expand All @@ -674,7 +677,26 @@ export class Renderer {
}

if (action.ref) {
set(this.refs, action.ref, { error });
const actionRef = get(this.refs, action.ref);
const retries = actionRef?._retries || 0;
set(this.refs, action.ref, {
...actionRef,
error,
_retries: retries + 1,
});
this.onTreeChange();

if (action.retryTimeout) {
if (
action.retryCount !== undefined
? retries < action.retryCount
: true
) {
setTimeout(() => {
this.stepIntoOrTriggerAction(action);
}, action.retryTimeout);
}
}
}

this.asyncAction = null;
Expand Down Expand Up @@ -1203,6 +1225,9 @@ export class Renderer {
{
type: "video",
videoSrc: this.replaceInlineContext(el.videoSrc),
mimeType: el.mimeType
? this.replaceInlineContext(el.mimeType)
: undefined,
},
key
);
Expand Down
41 changes: 5 additions & 36 deletions packages/react-ui-shadcn/src/renderers/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "video.js/dist/video-js.css";

interface PlayerProps {
videoSrc: string;
mimeType?: string;
}

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

const [videoSrc, setVideoSrc] = React.useState<string | undefined>();
const [overrideMimeType, setOverrideMimeType] = React.useState<
string | undefined
>(undefined);

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

const pollUrl = useCallback(
async (url: string) => {
const res = await fetch(url, { method: "HEAD" });
if (hasStartedPlaying) return;
if (res.ok) {
setVideoSrc(url);
} else {
setTimeout(() => {
pollUrl(url);
}, 1000);
}
},
[setVideoSrc, hasStartedPlaying]
);

const options = useMemo(
() => ({
...videoJSoptions,
Expand All @@ -62,7 +45,7 @@ export const VideoRenderer = (props: PlayerProps) => {
{
src: videoSrc ?? "",
type:
overrideMimeType ||
props.mimeType ||
(videoSrc?.endsWith(".m3u8")
? "application/x-mpegURL"
: videoSrc?.endsWith(".mp4")
Expand All @@ -71,26 +54,12 @@ export const VideoRenderer = (props: PlayerProps) => {
},
],
}),
[videoSrc, overrideMimeType]
[videoSrc, props.mimeType]
);

useEffect(() => {
if (props.videoSrc.startsWith("ipfs://")) {
// Exchange ipfs:// for .m3u8 url via /livepeer-video?url=ipfs://...
const baseUrl = `${
process.env.NEXT_PUBLIC_API_URL || "https://api.modprotocol.org"
}/livepeer-video`;
const endpointUrl = `${baseUrl}?url=${props.videoSrc}`;
fetch(endpointUrl).then(async (res) => {
const { url, fallbackUrl, mimeType } = await res.json();
setOverrideMimeType(mimeType);
setVideoSrc(`${fallbackUrl}`);
pollUrl(url);
});
} else {
setVideoSrc(props.videoSrc);
}
}, [props.videoSrc, pollUrl]);
setVideoSrc(props.videoSrc);
}, [props.videoSrc]);

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

0 comments on commit 2071ec1

Please sign in to comment.