t3xbu10dr: Publish video posts through the media pipeline and upgrade thumbnails to the player

This commit is contained in:
2026-10-19 14:49:03 -06:00
parent b4fb345f90
commit d493b35ede
9 changed files with 181 additions and 19 deletions
+46 -2
View File
@@ -9,8 +9,8 @@ import type { SessionData } from "./session-store";
// 3. the SERVER PUTs the bytes to that URL (the browser never gets the token)
// 4. confirm the upload (image confirmation blocks until processing finishes)
// 5. publish with the same {content} publish call as a text post
// The pipeline is media-type agnostic so the video task can reuse it; only the
// allowed-type gate and the public entry point here are image-specific.
// The pipeline is media-type agnostic; the allowed-type gates and entry points
// (createImagePost, createVideoPost) are per kind.
// Neither the feed token nor the file bytes are ever logged.
export type MediaKind = "image" | "video";
@@ -25,6 +25,12 @@ export const ALLOWED_IMAGE_TYPES: readonly string[] = [
"image/heic",
];
/** Video content types the feed accepts. */
export const ALLOWED_VIDEO_TYPES: readonly string[] = ["video/mp4", "video/quicktime", "video/webm"];
/** Upper bound on an uploaded video, enforced before any feed call. */
export const MAX_VIDEO_BYTES = 200 * 1024 * 1024;
/** Upper bound on an uploaded image, enforced before any feed call. */
export const MAX_IMAGE_BYTES = 15 * 1024 * 1024;
@@ -112,6 +118,44 @@ export function isAllowedImageType(type: string): boolean {
return ALLOWED_IMAGE_TYPES.includes(type.trim().toLowerCase());
}
/** True when the type is an accepted video content type (case-insensitive). */
export function isAllowedVideoType(type: string): boolean {
return ALLOWED_VIDEO_TYPES.includes(type.trim().toLowerCase());
}
/**
* Create a public video post: same pipeline as an image with type "video".
* Video confirmation may return while transcoding continues; the post is
* published anyway and the card shows the thumbnail until hlsUrl appears.
*/
export async function createVideoPost(
sessionId: string | undefined,
content: unknown,
file: MediaUpload["file"] | null,
options: RefreshOptions = {},
): Promise<CreateMediaPostResult> {
if (!file || typeof file.size !== "number" || file.size <= 0 || file.size > MAX_VIDEO_BYTES) {
return { ok: false, status: 400, error: "bad_file" };
}
if (typeof file.type !== "string" || !isAllowedVideoType(file.type)) {
return { ok: false, status: 400, error: "bad_file" };
}
return runMediaPipeline(sessionId, content, { file, mediaType: "video" }, options);
}
/** Route entry point: picks image or video from the file type; anything else is a 400. */
export async function createMediaPost(
sessionId: string | undefined,
content: unknown,
file: MediaUpload["file"] | null,
options: RefreshOptions = {},
): Promise<CreateMediaPostResult> {
if (file && typeof file.type === "string" && isAllowedVideoType(file.type)) {
return createVideoPost(sessionId, content, file, options);
}
return createImagePost(sessionId, content, file, options);
}
/**
* Create a public image post from a caption and one file: draft -> presigned URL
* -> server-side PUT -> confirm -> publish. Returns the post id so the caller can