miniflare
Version:
Fun, full-featured, fully-local simulator for Cloudflare Workers
258 lines (254 loc) • 8.7 kB
JavaScript
// src/workers/stream/binding.worker.ts
import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers";
import { getPublicUrl } from "miniflare:shared";
// src/workers/stream/errors.ts
var StreamBindingError = class extends Error {
constructor(message, code, statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.name = "StreamBindingError";
}
code;
statusCode;
}, BadRequestError = class extends StreamBindingError {
constructor(message = "Bad Request") {
super(message, 10005, 400), this.name = "BadRequestError";
}
};
var InvalidURLError = class extends StreamBindingError {
constructor(message = "Invalid URL") {
super(message, 10010, 400), this.name = "InvalidURLError";
}
};
// src/workers/stream/schemas.ts
function rowToStreamVideo(row, entryUrl) {
let placeholderUrl = `https://customer-placeholder.cloudflarestream.com/${row.id}`, videoUrl = `${entryUrl.origin}/cdn-cgi/mf/stream/${row.id}/watch`;
return {
id: row.id,
creator: row.creator,
thumbnail: row.thumbnail || `${placeholderUrl}/thumbnails/thumbnail.jpg`,
thumbnailTimestampPct: row.thumbnail_timestamp_pct,
readyToStream: row.ready_to_stream === 1,
readyToStreamAt: row.ready_to_stream_at,
status: {
state: row.status_state,
pctComplete: row.status_pct_complete ?? void 0,
errorReasonCode: row.status_error_reason_code,
errorReasonText: row.status_error_reason_text
},
meta: JSON.parse(row.meta),
created: row.created,
modified: row.modified,
scheduledDeletion: row.scheduled_deletion,
size: row.size,
preview: videoUrl,
allowedOrigins: JSON.parse(row.allowed_origins),
requireSignedURLs: row.require_signed_urls === null ? null : row.require_signed_urls === 1,
uploaded: row.uploaded,
uploadExpiry: row.upload_expiry,
maxSizeBytes: row.max_size_bytes,
maxDurationSeconds: row.max_duration_seconds,
duration: row.duration,
input: { width: row.input_width, height: row.input_height },
hlsPlaybackUrl: `${placeholderUrl}/manifest/video.m3u8`,
dashPlaybackUrl: `${placeholderUrl}/manifest/video.mpd`,
watermark: null,
liveInputId: row.live_input_id,
clippedFromId: row.clipped_from_id,
publicDetails: null
};
}
function rowToStreamCaption(row) {
return {
language: row.language,
label: row.label || row.language,
generated: row.generated === 1,
status: row.status
};
}
function rowToStreamWatermark(row) {
return {
id: row.id,
name: row.name,
size: row.size,
height: row.height,
width: row.width,
created: row.created,
downloadedFrom: row.downloaded_from,
opacity: row.opacity,
padding: row.padding,
scale: row.scale,
position: row.position
};
}
function rowToStreamDownload(row) {
return {
type: row.download_type,
download: {
percentComplete: row.percent_complete,
status: row.status,
url: row.url ?? void 0
}
};
}
// src/workers/stream/binding.worker.ts
function getStub(env) {
let id = env.store.idFromName("stream-data");
return env.store.get(id);
}
function rowsToDownloadResponse(rows) {
let result = {};
for (let { type, download } of rows)
result[type] = download;
return result;
}
var StreamBinding = class extends WorkerEntrypoint {
async fetch(request) {
let match = new URL(request.url).pathname.match(/^\/cdn-cgi\/mf\/stream\/([^/]+)\/watch$/);
if (!match)
return new Response("Not found", { status: 404 });
let videoId = match[1], result = await getStub(this.env).getVideoBlob(videoId);
return result === null ? new Response("Video not found", { status: 404 }) : new Response(result.stream, {
headers: {
"Content-Type": "video/mp4",
"Content-Length": result.size.toString()
}
});
}
async upload(urlOrBody, params) {
let body;
if (typeof urlOrBody == "string") {
let response = await fetch(urlOrBody);
if (!response.ok || response.body === null)
throw new InvalidURLError(
`Failed to fetch video from URL: ${response.status} ${response.statusText}`
);
body = response.body;
} else
body = urlOrBody;
let row = await getStub(this.env).createVideo(body, params ?? {}), entryUrl = await getPublicUrl(this.env.MINIFLARE_LOOPBACK);
return rowToStreamVideo(row, entryUrl);
}
// Not supported in local mode yet
async createDirectUpload(_params) {
throw new BadRequestError(
"createDirectUpload is not supported in local mode"
);
}
video(id) {
return new StreamVideoHandleImpl(this.env, id);
}
get videos() {
return new StreamVideosImpl(this.env);
}
get watermarks() {
return new StreamWatermarksImpl(this.env);
}
}, StreamScopedCaptionsImpl = class extends RpcTarget {
#env;
#videoId;
constructor(env, videoId) {
super(), this.#env = env, this.#videoId = videoId;
}
async upload(language, input) {
let row = await getStub(this.#env).uploadCaption(this.#videoId, language, input);
return rowToStreamCaption(row);
}
async generate(language) {
let row = await getStub(this.#env).generateCaption(this.#videoId, language);
return rowToStreamCaption(row);
}
async list(language) {
return (await getStub(this.#env).listCaptions(this.#videoId, language)).map(rowToStreamCaption);
}
async delete(language) {
await getStub(this.#env).deleteCaption(this.#videoId, language);
}
}, StreamScopedDownloadsImpl = class extends RpcTarget {
#env;
#videoId;
constructor(env, videoId) {
super(), this.#env = env, this.#videoId = videoId;
}
async generate(downloadType = "default") {
let rows = await getStub(this.#env).generateDownload(this.#videoId, downloadType);
return rowsToDownloadResponse(rows.map(rowToStreamDownload));
}
async get() {
let rows = await getStub(this.#env).listDownloads(this.#videoId);
return rowsToDownloadResponse(rows.map(rowToStreamDownload));
}
async delete(downloadType = "default") {
await getStub(this.#env).deleteDownload(this.#videoId, downloadType);
}
}, StreamVideoHandleImpl = class extends RpcTarget {
id;
#env;
constructor(env, id) {
super(), this.#env = env, this.id = id;
}
async details() {
let row = await getStub(this.#env).getVideo(this.id), entryUrl = await getPublicUrl(this.#env.MINIFLARE_LOOPBACK);
return rowToStreamVideo(row, entryUrl);
}
async update(params) {
let row = await getStub(this.#env).updateVideo(this.id, params), entryUrl = await getPublicUrl(this.#env.MINIFLARE_LOOPBACK);
return rowToStreamVideo(row, entryUrl);
}
async delete() {
await getStub(this.#env).deleteVideo(this.id);
}
async generateToken() {
return getStub(this.#env).generateToken(this.id);
}
get downloads() {
return new StreamScopedDownloadsImpl(this.#env, this.id);
}
get captions() {
return new StreamScopedCaptionsImpl(this.#env, this.id);
}
}, StreamVideosImpl = class extends RpcTarget {
#env;
constructor(env) {
super(), this.#env = env;
}
async list(params) {
let rows = await getStub(this.#env).listVideos(params), entryUrl = await getPublicUrl(this.#env.MINIFLARE_LOOPBACK);
return rows.map((row) => rowToStreamVideo(row, entryUrl));
}
}, StreamWatermarksImpl = class extends RpcTarget {
#env;
constructor(env) {
super(), this.#env = env;
}
async generate(streamOrUrl, params) {
if (params.opacity !== void 0 && (params.opacity < 0 || params.opacity > 1))
throw new BadRequestError("opacity must be between 0.0 and 1.0");
if (params.padding !== void 0 && (params.padding < 0 || params.padding > 1))
throw new BadRequestError("padding must be between 0.0 and 1.0");
if (params.scale !== void 0 && (params.scale < 0 || params.scale > 1))
throw new BadRequestError("scale must be between 0.0 and 1.0");
let stub = getStub(this.#env);
if (typeof streamOrUrl == "string") {
let row2 = await stub.createWatermarkFromUrl(streamOrUrl, params);
return rowToStreamWatermark(row2);
}
let buffer = await new Response(streamOrUrl).arrayBuffer(), row = await stub.createWatermarkFromBody(buffer, null, params);
return rowToStreamWatermark(row);
}
async list() {
return (await getStub(this.#env).listWatermarks()).map(rowToStreamWatermark);
}
async get(watermarkId) {
let row = await getStub(this.#env).getWatermark(watermarkId);
return rowToStreamWatermark(row);
}
async delete(watermarkId) {
await getStub(this.#env).deleteWatermark(watermarkId);
}
};
export {
StreamBinding
};
//# sourceMappingURL=binding.worker.js.map