Skip to content

Commit

Permalink
feat: Stitcher compatible with live streams (#122)
Browse files Browse the repository at this point in the history
* Added support for multiple tags in EXTINF segment group

* Refactored manual interstitials to support CUE

* Error early when KV is not available on CF worker

* Remove unused env variables for test

* Simplified filter

* Change content-type

* Made sessionId optional

* Do not expose outUrl

* Added error log in app
  • Loading branch information
matvp91 authored Nov 22, 2024
1 parent da6e66e commit 4958411
Show file tree
Hide file tree
Showing 30 changed files with 394 additions and 358 deletions.
Binary file modified bun.lockb
Binary file not shown.
9 changes: 3 additions & 6 deletions packages/api/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { parseEnv } from "shared/env";

export const env = parseEnv((z) => ({
// process
PORT: z.coerce.number().default(52001),
HOST: z.string().default("0.0.0.0"),

// config.env
REDIS_HOST: z.string(),
REDIS_PORT: z.coerce.number(),
HOST: z.string().optional(),
REDIS_HOST: z.string().default("localhost"),
REDIS_PORT: z.coerce.number().default(6379),
S3_ENDPOINT: z.string(),
S3_REGION: z.string(),
S3_ACCESS_KEY: z.string(),
Expand Down
29 changes: 24 additions & 5 deletions packages/app/src/routes/(dashboard)/_layout/player.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Card } from "@nextui-org/react";
import { Card, Modal, ModalBody, ModalContent } from "@nextui-org/react";
import { createFileRoute } from "@tanstack/react-router";
import { useRef, useState } from "react";
import { CodeEditor } from "../../../components/CodeEditor";
Expand All @@ -14,14 +14,15 @@ export const Route = createFileRoute("/(dashboard)/_layout/player")({
function RouteComponent() {
const formRef = useRef<FormRef>(null);
const [url, setUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const schema = useSwaggerSchema(
`${window.__ENV__.PUBLIC_STITCHER_ENDPOINT}/swagger/json`,
"/session",
);

return (
<div className="p-8 h-screen flex gap-4">
<div className="h-screen p-8 flex gap-4">
<div className="grow">
<Player url={url} lang="eng" metadata={{}} />
<Card className="mt-4 p-4">
Expand All @@ -46,6 +47,8 @@ function RouteComponent() {
schema={schema}
localStorageKey="stitcherEditor"
onSave={async (body) => {
setError(null);

const response = await fetch(
`${window.__ENV__.PUBLIC_STITCHER_ENDPOINT}/session`,
{
Expand All @@ -57,14 +60,30 @@ function RouteComponent() {
},
);

const data = await response.json();
if (response.ok) {
const { url } = await response.json();
formRef.current?.setValue("url", url);
setUrl(url);
formRef.current?.setValue("url", data.url);
setUrl(data.url);
} else {
setError(data);
}
}}
/>
</Card>
<Modal
isOpen={error !== null}
onClose={() => setError(null)}
size="5xl"
scrollBehavior="inside"
>
<ModalContent>
<ModalBody className="p-4">
<pre className="text-xs text-red-500">
{JSON.stringify(error, null, 2)}
</pre>
</ModalBody>
</ModalContent>
</Modal>
</div>
);
}
5 changes: 2 additions & 3 deletions packages/artisan/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { parseEnv } from "shared/env";

export const env = parseEnv((z) => ({
// config.env
REDIS_HOST: z.string().default("localhost"),
REDIS_PORT: z.coerce.number().default(6379),
S3_ENDPOINT: z.string(),
S3_REGION: z.string(),
S3_ACCESS_KEY: z.string(),
S3_SECRET_KEY: z.string(),
S3_BUCKET: z.string(),
REDIS_HOST: z.string(),
REDIS_PORT: z.coerce.number(),
}));
4 changes: 2 additions & 2 deletions packages/bolt/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { parseEnv } from "shared/env";

const env = parseEnv((z) => ({
// config.env
REDIS_HOST: z.string(),
REDIS_PORT: z.coerce.number(),
REDIS_HOST: z.string().default("localhost"),
REDIS_PORT: z.coerce.number().default(6379),
}));

export const connection = {
Expand Down
2 changes: 1 addition & 1 deletion packages/stitcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@
"@matvp91/elysia-swagger": "^2.0.0",
"@superstreamer/api": "workspace:*",
"@xmldom/xmldom": "^0.8.10",
"cryptr": "^6.3.0",
"dom-parser": "^1.1.5",
"elysia": "^1.1.24",
"hh-mm-ss": "^1.2.0",
"lru-cache": "^11.0.2",
"luxon": "^3.5.0",
"redis": "^4.7.0",
"secure-encrypt": "^1.0.12",
"shared": "workspace:*",
"superjson": "^2.2.1",
"uuid": "^10.0.0",
Expand Down
7 changes: 1 addition & 6 deletions packages/stitcher/runtime/local.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { parseEnv } from "shared/env";
import { createApp } from "../src";

const env = parseEnv((z) => ({
PORT: z.coerce.number().default(52002),
HOST: z.string().default("0.0.0.0"),
}));
import { env } from "../src/env";

const app = createApp({
aot: true,
Expand Down
21 changes: 21 additions & 0 deletions packages/stitcher/src/adapters/kv/cloudflare-kv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { KVNamespace } from "@cloudflare/workers-types";

// Make sure wrangler.toml has a binding named "kv".
const kv = process.env["kv"] as unknown as KVNamespace;

if (!kv) {
throw new ReferenceError(
'No kv found for Cloudflare, make sure you have "kv"' +
" set as binding in wrangler.toml.",
);
}

export async function set(key: string, value: string, ttl: number) {
await kv.put(key, value, {
expirationTtl: ttl,
});
}

export async function get(key: string) {
return await kv.get(key);
}
15 changes: 15 additions & 0 deletions packages/stitcher/src/adapters/kv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { env } from "../../env";

interface KeyValue {
set(key: string, value: string, ttl: number): Promise<void>;
get(key: string): Promise<string | null>;
}

export let kv: KeyValue;

// Map each KV adapter here to their corresponding import.
if (env.KV === "cloudflare-kv") {
kv = await import("./cloudflare-kv");
} else if (env.KV === "redis") {
kv = await import("./redis");
}
23 changes: 23 additions & 0 deletions packages/stitcher/src/adapters/kv/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createClient } from "redis";
import { env } from "../../env";

const REDIS_PREFIX = "stitcher";

const client = createClient({
socket: {
host: env.REDIS_HOST,
port: env.REDIS_PORT,
},
});

await client.connect();

export async function set(key: string, value: string, ttl: number) {
await client.set(`${REDIS_PREFIX}:${key}`, value, {
EX: ttl,
});
}

export async function get(key: string) {
return await client.get(`${REDIS_PREFIX}:${key}`);
}
9 changes: 6 additions & 3 deletions packages/stitcher/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { parseEnv } from "shared/env";

export const env = parseEnv((z) => ({
SERVERLESS: z.coerce.boolean().default(false),
PORT: z.coerce.number().default(52002),
HOST: z.string().optional(),

KV: z.enum(["redis", "cloudflare-kv"]).default("redis"),
REDIS_HOST: z.string().default("localhost"),
REDIS_PORT: z.coerce.number().default(6379),

REDIS_HOST: z.string(),
REDIS_PORT: z.coerce.number(),
PUBLIC_S3_ENDPOINT: z.string(),
PUBLIC_STITCHER_ENDPOINT: z.string(),
PUBLIC_API_ENDPOINT: z.string(),
Expand Down
36 changes: 15 additions & 21 deletions packages/stitcher/src/filters.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { t } from "elysia";
import type { MasterPlaylist } from "./parser";

export interface Filter {
resolution?: string;
audioLanguage?: string;
}

export function formatFilterToQueryParam(filter?: Filter) {
if (!filter) {
return undefined;
}
return btoa(JSON.stringify(filter));
}

export const filterSchema = t.Optional(
t
.Transform(t.String())
.Decode((value) => JSON.parse(atob(value)) as Filter)
.Encode((filter) => btoa(JSON.stringify(filter))),
);

function parseRange(input: string): [number, number] | null {
const match = input.match(/^(\d+)-(\d+)$/);

Expand Down Expand Up @@ -77,24 +92,3 @@ export function filterMasterPlaylist(master: MasterPlaylist, filter: Filter) {
});
}
}

export function getFilterFromQuery(query: Record<string, string>) {
const filter: Filter = {};
if ("filter.resolution" in query) {
filter.resolution = query["filter.resolution"];
}
if ("filter.audioLanguage" in query) {
filter.audioLanguage = query["filter.audioLanguage"];
}
return filter;
}

export function getQueryParamsFromFilter(filter: Filter) {
const queryParams: Record<string, string> = {};

Object.entries(filter).forEach(([key, value]) => {
queryParams[`filter.${key}`] = value;
});

return queryParams;
}
Loading

0 comments on commit 4958411

Please sign in to comment.