UNPKG

next-video

Version:

A React component for adding video to your Next.js application. It extends both the video element and your Next app with features for automatic video optimization.

202 lines (201 loc) 7.32 kB
import { input, confirm } from "@inquirer/prompts"; import chalk from "chalk"; import { fetch } from "undici"; import path from "node:path"; import log from "../utils/logger.js"; import { getVideoConfig } from "../config.js"; import { transform as muxTransform } from "../providers/mux/transformer.js"; const PLAYBACK_ID_REGEX = /^[a-zA-Z0-9]{22,}$/; function sanitizeFilename(title) { return title.replace(/[^a-zA-Z0-9\s\-_]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase(); } function sanitizeImportName(filename) { const nameWithoutExt = filename.replace(/\.mp4$/, ""); return nameWithoutExt.replace(/[^a-zA-Z0-9\s\-_]/g, "").replace(/[-\s]+(.)/g, (_, char) => char.toUpperCase()).replace(/^[^a-zA-Z_$]/, "_").replace(/[^a-zA-Z0-9_$]/g, "_"); } async function fetchMuxMetadata(playbackId) { try { const response = await fetch(`https://stream.mux.com/${playbackId}/metadata.json`); if (!response.ok) { return null; } const metadata = await response.json(); const title = metadata?.[0]?.titles?.[0]?.title; return title ? sanitizeFilename(title) : null; } catch (error) { return null; } } const command = "adopt <playbackId>"; const desc = "Create a local asset metadata file for an existing Mux asset using its playback ID."; function builder(yargs) { return yargs.positional("playbackId", { describe: "The Mux playback ID of the asset to adopt", type: "string", demandOption: true }).options({ dir: { alias: "d", describe: "The directory to save the asset metadata file.", type: "string", default: "videos" }, name: { alias: "n", describe: "Custom name for the asset file (without extension).", type: "string" }, "thumbnail-time": { alias: "t", describe: "Thumbnail time in seconds for the poster image.", type: "number" }, interactive: { alias: "i", describe: "Run in interactive mode to configure all options.", type: "boolean", default: true } }); } async function handler(argv) { const playbackId = argv.playbackId; const dir = argv.dir; const interactive = argv.interactive; if (!PLAYBACK_ID_REGEX.test(playbackId)) { log.error("Invalid playback ID format. Mux playback IDs should be alphanumeric and at least 22 characters long."); return; } log.info( `${chalk.magenta.bold("next-video adopt")}: Creating asset metadata for playback ID: ${chalk.cyan(playbackId)}` ); log.space(); log.info("Fetching asset metadata from Mux..."); const muxTitle = await fetchMuxMetadata(playbackId); if (muxTitle) { log.info(`Found title: ${chalk.cyan(muxTitle)} (sanitized for filename)`); } else { log.info("No metadata found or unable to fetch from Mux"); } log.space(); try { const videoConfig = await getVideoConfig(); if (videoConfig.provider !== "mux") { log.warning( `Current provider is "${videoConfig.provider}". The adopt command is currently only compatible with Mux assets.` ); if (interactive) { const proceed = await confirm({ message: "Do you want to proceed anyway?", default: false }); if (!proceed) { log.info("Adoption cancelled."); return; } } } let assetName = argv.name; let thumbnailTime = argv["thumbnail-time"]; if (interactive) { if (!assetName) { const defaultName = muxTitle || `adopted-${playbackId.slice(0, 8)}`; assetName = await input({ message: "Asset name (filename without extension):", default: defaultName, validate: (input2) => { if (!input2.trim()) return "Asset name is required"; if (input2.includes("/") || input2.includes("\\")) return "Asset name should not contain path separators"; return true; } }); } const configureThumbnail = await confirm({ message: "Do you want to configure a custom thumbnail time?", default: false }); if (configureThumbnail && thumbnailTime === void 0) { const thumbnailInput = await input({ message: "Thumbnail time in seconds:", default: "0", validate: (input2) => { const num = parseFloat(input2); if (isNaN(num) || num < 0) return "Please enter a valid number >= 0"; return true; } }); thumbnailTime = parseFloat(thumbnailInput); } } else { assetName = assetName || muxTitle || `adopted-${playbackId.slice(0, 8)}`; } const originalPath = `${dir}/${assetName}.mp4`; const now = Date.now(); const baseAsset = { status: "ready", originalFilePath: originalPath, provider: "mux", providerMetadata: { mux: { playbackId } }, createdAt: now, updatedAt: now }; if (thumbnailTime !== void 0) { baseAsset.providerMetadata.mux.thumbnailTime = thumbnailTime; } const transformedAsset = muxTransform(baseAsset, { thumbnailTime }); try { const { createThumbHash } = await import("../providers/mux/provider.js"); const blurDataURL = await createThumbHash( `https://image.mux.com/${playbackId}/thumbnail.webp?width=16&height=16` ); transformedAsset.blurDataURL = blurDataURL; } catch (error) { log.warning("Could not generate blur data URL for thumbnail"); } const assetFilePath = path.join(dir, `${assetName}.mp4`); const assetConfigPath = `${assetFilePath}.json`; await videoConfig.saveAsset(assetConfigPath, transformedAsset); log.success(`Asset metadata created: ${chalk.cyan(assetConfigPath)}`); log.space(); log.info("Asset details:"); log.space(chalk.gray(">"), log.label("Playback ID:"), playbackId); log.space(chalk.gray(">"), log.label("Original Path:"), originalPath); log.space(chalk.gray(">"), log.label("Video Source:"), transformedAsset.sources?.[0]?.src || "N/A"); log.space(chalk.gray(">"), log.label("Poster:"), transformedAsset.poster || "N/A"); if (thumbnailTime !== void 0) { log.space(chalk.gray(">"), log.label("Thumbnail Time:"), `${thumbnailTime}s`); } log.space(); log.info(`You can now import this asset in your Next.js application:`); log.space(); const importName = sanitizeImportName(assetName); log.space(`${chalk.magenta("import")} Video ${chalk.magenta("from")} ${chalk.cyan("'next-video'")};`); log.space( `${chalk.magenta("import")} ${importName} ${chalk.magenta("from")} ${chalk.cyan(`'/${assetFilePath}'`)};` ); log.space(); log.space(`${chalk.magenta("export default function")} Page() {`); log.space( ` ${chalk.magenta("return")} ${chalk.cyan("<")}Video ${chalk.cyan("src=")}{${importName}} ${chalk.cyan("/>")};` ); log.space(`}`); log.space(); } catch (error) { log.error("Failed to adopt asset:", error.message); if (error.code === "ENOENT" && error.path?.includes(dir)) { log.info( `Directory ${chalk.bold(dir)} does not exist. You may need to run ${chalk.bold.magenta("next-video init")} first.` ); } } } export { builder, command, desc, handler };