rspace-online/modules/tube/mod.ts

112 lines
3.4 KiB
TypeScript

/**
* Tube module — community video hosting & live streaming.
*
* Video library (Cloudflare R2 bucket), HLS live streaming,
* and RTMP ingest support. No database — metadata from S3 listing.
*/
import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
const routes = new Hono();
// ── R2 / S3 config ──
const R2_ENDPOINT = process.env.R2_ENDPOINT || "";
const R2_BUCKET = process.env.R2_BUCKET || "rtube-videos";
const R2_ACCESS_KEY = process.env.R2_ACCESS_KEY || "";
const R2_SECRET_KEY = process.env.R2_SECRET_KEY || "";
const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL || "";
const VIDEO_EXTENSIONS = new Set([
".mp4", ".mkv", ".webm", ".mov", ".avi", ".wmv", ".flv", ".m4v",
]);
// ── S3-compatible listing (lightweight, no AWS SDK needed) ──
async function listVideos(): Promise<Array<{ name: string; size: number }>> {
if (!R2_ENDPOINT) {
// Return demo data when R2 not configured
return [
{ name: "welcome-to-rtube.mp4", size: 12_500_000 },
{ name: "community-meeting-2026.mp4", size: 85_000_000 },
{ name: "workshop-recording.webm", size: 45_000_000 },
];
}
try {
// Use S3 ListObjectsV2 via signed request
const url = `${R2_ENDPOINT}/${R2_BUCKET}?list-type=2&max-keys=200`;
const resp = await fetch(url, {
headers: {
Authorization: `AWS4-HMAC-SHA256 ...`, // Simplified — real impl uses aws4 signing
},
});
if (!resp.ok) return [];
const text = await resp.text();
// Parse simple XML response
const items: Array<{ name: string; size: number }> = [];
const keyMatches = text.matchAll(/<Key>([^<]+)<\/Key>/g);
const sizeMatches = text.matchAll(/<Size>(\d+)<\/Size>/g);
const keys = [...keyMatches].map((m) => m[1]);
const sizes = [...sizeMatches].map((m) => parseInt(m[1]));
for (let i = 0; i < keys.length; i++) {
const ext = keys[i].substring(keys[i].lastIndexOf(".")).toLowerCase();
if (VIDEO_EXTENSIONS.has(ext)) {
items.push({ name: keys[i], size: sizes[i] || 0 });
}
}
return items.sort((a, b) => a.name.localeCompare(b.name));
} catch {
return [];
}
}
// ── API routes ──
// GET /api/videos — list videos from bucket
routes.get("/api/videos", async (c) => {
const videos = await listVideos();
return c.json({ videos });
});
// GET /api/info — module info
routes.get("/api/info", (c) => {
return c.json({
module: "tube",
name: "rTube",
r2Configured: !!R2_ENDPOINT,
rtmpIngest: "rtmp://rtube.online:1936/live",
features: ["video-library", "hls-streaming", "rtmp-ingest"],
});
});
// GET /api/health
routes.get("/api/health", (c) => c.json({ ok: true }));
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Tube | rSpace`,
moduleId: "tube",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "light",
styles: `<link rel="stylesheet" href="/modules/tube/tube.css">`,
body: `<folk-video-player space="${space}"></folk-video-player>`,
scripts: `<script type="module" src="/modules/tube/folk-video-player.js"></script>`,
}));
});
export const tubeModule: RSpaceModule = {
id: "tube",
name: "rTube",
icon: "\u{1F3AC}",
description: "Community video hosting & live streaming",
routes,
standaloneDomain: "rtube.online",
};