t3xbu10dr: Publish video posts through the media pipeline and upgrade thumbnails to the player
This commit is contained in:
+33
-1
@@ -3,7 +3,7 @@ import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { FeedView, type FeedViewProps } from "@/components/FeedView";
|
||||
import { fetchFeedPage, FeedLoadError } from "@/lib/feed-client";
|
||||
import { mergePosts, relativeTime, toCardPosts } from "@/lib/feed-model";
|
||||
import { mergePosts, relativeTime, toCardPosts, upgradeVideos } from "@/lib/feed-model";
|
||||
|
||||
const NOW = Date.parse("2026-10-19T12:00:00Z");
|
||||
const post = (id: string, publishedAt: string, extra: Record<string, unknown> = {}) => ({
|
||||
@@ -137,3 +137,35 @@ describe("fetchFeedPage", () => {
|
||||
await expect(fetchFeedPage(null, mk(500))).rejects.toMatchObject({ signedOut: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("video transcoding", () => {
|
||||
const vid = (hls: unknown) =>
|
||||
toCardPosts([post("7", "2026-10-19T10:00:00Z", { mediaItems: [{ mediaType: "video", thumbnailUrl: "https://i/t.jpg", hlsUrl: hls }] })]);
|
||||
|
||||
it("keeps a just-published video as a thumbnail with no hlsUrl", () => {
|
||||
const [p] = vid(null);
|
||||
expect(p.media).toEqual([{ kind: "video", src: "https://i/t.jpg", hlsUrl: null }]);
|
||||
});
|
||||
|
||||
it("keeps a video with neither thumbnail nor hlsUrl as a pending card", () => {
|
||||
const [p] = toCardPosts([post("8", "2026-10-19T10:00:00Z", { mediaItems: [{ mediaType: "video" }] })]);
|
||||
expect(p.media).toHaveLength(1);
|
||||
expect(p.media[0].src).toBeNull();
|
||||
});
|
||||
|
||||
it("upgrades the shown thumbnail to the player when hlsUrl appears", () => {
|
||||
const shown = vid(null);
|
||||
const next = upgradeVideos(shown, vid("https://stream.mux.com/abc"));
|
||||
expect(next[0].media[0].hlsUrl).toContain("stream.mux.com");
|
||||
expect(upgradeVideos(next, vid("https://stream.mux.com/abc"))).toBe(next);
|
||||
expect(upgradeVideos(shown, vid(null))).toBe(shown);
|
||||
});
|
||||
|
||||
it("renders the thumbnail while transcoding and the player once hlsUrl is present", () => {
|
||||
const html = render({ posts: vid(null) });
|
||||
expect(html).toContain("https://i/t.jpg");
|
||||
expect(html).not.toContain("<video");
|
||||
const live = render({ posts: vid("https://stream.mux.com/abc") });
|
||||
expect(live).toContain("<video");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
import { POST } from "@/app/api/posts/route";
|
||||
import { createImagePost } from "@/lib/post-media";
|
||||
import { createImagePost, createVideoPost } from "@/lib/post-media";
|
||||
import { createSession } from "@/lib/session-store";
|
||||
|
||||
// Server-side image-post pipeline, with a fully mocked feed. The browser sends
|
||||
@@ -181,3 +181,52 @@ describe("POST /api/posts (image)", () => {
|
||||
expect(bad).toEqual({ ok: false, status: 400, error: "bad_file" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/posts (video)", () => {
|
||||
const videoFile = (name = "clip.mp4", type = "video/mp4") =>
|
||||
new File([new Uint8Array([1, 2, 3])], name, { type });
|
||||
|
||||
function videoFeed(bodies: Record<string, unknown>, calls: string[]) {
|
||||
return vi.fn(async (url: string | URL, init?: RequestInit) => {
|
||||
const u = String(url);
|
||||
calls.push(u);
|
||||
if (u.includes("/api/v1/") && typeof init?.body === "string") bodies[new URL(u).pathname] = JSON.parse(init.body);
|
||||
if (u.endsWith("/api/v1/posts")) return jsonRes({ data: { id: "v1", status: "draft" } }, 201);
|
||||
if (u.endsWith("/attachments/presigned-urls"))
|
||||
return jsonRes({ data: [{ attachmentId: "a1", s3Key: "k/1.mp4", url: "https://s3.example/put" }] }, 200);
|
||||
if (u === "https://s3.example/put") return new Response(null, { status: 200 });
|
||||
// Confirmation returns while transcoding continues: no hlsUrl.
|
||||
if (u.endsWith("/confirm-uploads")) return jsonRes({ data: { status: "processing" } }, 200);
|
||||
return jsonRes({ data: { success: true } }, 200);
|
||||
});
|
||||
}
|
||||
|
||||
it("uses draft type video, confirms mediaType video, and publishes without hlsUrl", async () => {
|
||||
const sid = session();
|
||||
const bodies: Record<string, unknown> = {};
|
||||
const calls: string[] = [];
|
||||
const fn = videoFeed(bodies, calls);
|
||||
const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("hi", videoFile("c.mov", "video/quicktime"), sid)));
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ id: "v1" });
|
||||
expect((bodies["/api/v1/posts"] as { type: string }).type).toBe("video");
|
||||
const uploads = (bodies["/api/v1/attachments/confirm-uploads"] as { uploads: { mediaType: string }[] }).uploads;
|
||||
expect(uploads[0].mediaType).toBe("video");
|
||||
expect(calls.some((u) => u.endsWith("/api/v1/posts/v1/publish"))).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts mp4, quicktime and webm; rejects other video types before any feed call", async () => {
|
||||
const sid = session();
|
||||
const ok = videoFeed({}, []) as unknown as typeof fetch;
|
||||
for (const t of ["video/mp4", "video/quicktime", "video/webm"]) {
|
||||
expect((await createVideoPost(sid, "c", videoFile("f", t), { fetchImpl: ok })).ok).toBe(true);
|
||||
}
|
||||
const fn = vi.fn();
|
||||
const res = await withFetch(fn as unknown as typeof fetch, () =>
|
||||
POST(multipart("x", videoFile("a.avi", "video/x-msvideo"), sid)),
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toEqual({ error: "bad_file" });
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user