import type * as cf from "@cloudflare/workers-types"; import * as Data from "effect/Data"; import type * as Effect from "effect/Effect"; import type { RuntimeContext } from "../../RuntimeContext.ts"; import * as Binding from "../Workers/Binding.ts"; import type { StreamBinding } from "./StreamBinding.ts"; const TypeId = "Cloudflare.Stream.Stream" as const; type TypeId = typeof TypeId; /** * Error produced by a {@link StreamClient} operation. Mirrors the runtime's * `StreamBindingError` shape (`code` is the Cloudflare Stream error code, * `statusCode` the HTTP status it maps to) when the failure crosses the * binding boundary. */ export class StreamError extends Data.TaggedError("StreamError")<{ message: string; code?: number; statusCode?: number; cause: unknown; }> {} /** An Effect produced by a {@link StreamClient} operation. */ type StreamEffect = Effect.Effect; /** * A Cloudflare Stream binding for managing videos, captions, downloads and * watermarks from Workers — a Worker-only binding with no backing cloud * resource. * * `Stream` is a single value that is at once the `Binding.Service` tag, the * callable that produces a {@link StreamBinding}, and the type. Declare it on a * Worker's `env` (it flows through `InferEnv` → the runtime `StreamBinding` * handle) or `yield*` it inside an Effect-native Worker to attach the binding * and obtain the {@link StreamClient}. * * In `alchemy dev` the binding is emulated locally: uploads land in a local * video store and each video's `preview` URL is served unmodified at * `{devUrl}/cdn-cgi/mf/stream//watch`. The local store performs no * transcoding (the `hlsPlaybackUrl`/`dashPlaybackUrl` point at a placeholder * host), no signed URLs, and `createDirectUpload` is unsupported. Pipe the * binding through `Alchemy.remote()` to proxy to the real Stream service * instead. * * ### Effect-style Worker (recommended) * **Example:** Upload a video by URL and read back its details * ```typescript * import * as Effect from "effect/Effect"; * * Cloudflare.Worker( * "StreamWorker", * { main: import.meta.url }, * Effect.gen(function* () { * const stream = yield* Cloudflare.Stream.Stream("STREAM"); * * return { * fetch: Effect.gen(function* () { * const video = yield* stream.upload("https://example.com/video.mp4"); * const details = yield* stream.video(video.id).details(); * return yield* HttpServerResponse.json(details); * }), * }; * }).pipe(Effect.provide(Cloudflare.Stream.StreamBinding)), * ); * ``` * * ### Worker binding metadata * **Example:** Declare the binding on `env` * ```typescript * export const Worker = Cloudflare.Worker("Worker", { * main: "./src/worker.ts", * env: { STREAM: Cloudflare.Stream.Stream() }, * }); * * export type WorkerEnv = Cloudflare.InferEnv; * // { STREAM: StreamBinding } * ``` * * ### Local development * **Example:** Proxy to the real Stream service in dev * ```typescript * // Default: videos land in the local video store under `alchemy dev`. * // Alchemy.remote() opts the binding into the real Stream service * // instead — in an Effect-native Worker: * const stream = yield* Cloudflare.Stream.Stream("STREAM").pipe(Alchemy.remote()); * * // or declared on an async Worker's env: * env: { STREAM: Cloudflare.Stream.Stream("STREAM").pipe(Alchemy.remote()) } * ``` * * @see https://developers.cloudflare.com/stream/ * * @binding * @product Stream * @category Media */ export interface Stream extends Binding.Service { /** * @param name Binding name (logical id) — the `env` key it resolves to. * @default "STREAM" */ (name?: string): StreamBinding; } export const Stream = Binding.Service({ id: TypeId, defaultName: "STREAM", toWorkerBinding: (binding) => ({ type: "stream", name: binding.name, }), }); export const isStream = (value: unknown): value is StreamBinding => Binding.isBinding(value) && value.kind === TypeId; /** * Effect-native client for a Cloudflare Stream binding. Mirrors the runtime * {@link cf.StreamBinding} handle: each operation returns an Effect tagged with * {@link StreamError}; `video(id)` returns a pure per-video handle. */ export interface StreamClient { /** Effect resolving to the raw Cloudflare Stream runtime binding. */ raw: Effect.Effect; /** Upload a new video from a URL. */ upload( url: string, params?: cf.StreamUrlUploadParams, ): StreamEffect; /** * Create a direct-creator upload (video uploads without an API key). * Unsupported by the local dev simulator — fails with {@link StreamError}. */ createDirectUpload( params: cf.StreamDirectUploadCreateParams, ): StreamEffect; /** A handle scoped to a single video for per-video operations. */ video(id: string): StreamVideoClient; /** Account-level video operations. */ videos: { /** List videos. */ list(params?: cf.StreamVideosListParams): StreamEffect; }; /** Watermark profile operations. */ watermarks: { /** Create a watermark profile from an image stream or URL. */ generate( input: ReadableStream | string, params: cf.StreamWatermarkCreateParams, ): StreamEffect; /** List watermark profiles. */ list(): StreamEffect; /** Get a watermark profile. */ get(watermarkId: string): StreamEffect; /** Delete a watermark profile. */ delete(watermarkId: string): StreamEffect; }; } /** Effect-native handle for operations scoped to a single Stream video. */ export interface StreamVideoClient { /** The unique identifier for the video. */ readonly id: string; /** Get the full video details. */ details(): StreamEffect; /** Update details for the video. */ update(params: cf.StreamUpdateVideoParams): StreamEffect; /** Delete the video and its copies. */ delete(): StreamEffect; /** Create a signed URL token for the video. */ generateToken(): StreamEffect; /** MP4 download operations for the video. */ downloads: { /** Generate a download of the given type. */ generate( downloadType?: cf.StreamDownloadType, ): StreamEffect; /** Get the video's downloads. */ get(): StreamEffect; /** Delete a download of the given type. */ delete(downloadType?: cf.StreamDownloadType): StreamEffect; }; /** Caption operations for the video. */ captions: { /** Upload a WebVTT caption file for a language. */ upload( language: string, input: ReadableStream, ): StreamEffect; /** Generate a caption via AI for a language. */ generate(language: string): StreamEffect; /** List the video's captions, optionally filtered by language. */ list(language?: string): StreamEffect; /** Delete the caption for a language. */ delete(language: string): StreamEffect; }; }