Skip to content

Instantly share code, notes, and snippets.

@mozzius
Created October 18, 2024 20:48
Show Gist options
  • Save mozzius/5cbbd15e12cdc0cb1d0d992b7c3b1d0f to your computer and use it in GitHub Desktop.
Save mozzius/5cbbd15e12cdc0cb1d0d992b7c3b1d0f to your computer and use it in GitHub Desktop.

Revisions

  1. mozzius created this gist Oct 18, 2024.
    119 changes: 119 additions & 0 deletions happy-path.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,119 @@
    import {
    AppBskyEmbedVideo,
    AppBskyVideoDefs,
    AtpAgent,
    BlobRef,
    } from "npm:@atproto/api";

    const userAgent = new AtpAgent({
    service: prompt("Service URL (default: https://bsky.social):") ||
    "https://bsky.social",
    });

    await userAgent.login({
    identifier: prompt("Handle:")!,
    password: prompt("Password:")!,
    });

    console.log(`Logged in as ${userAgent.session?.handle}`);

    const videoPath = prompt("Video file (.mp4):")!;

    const { data: serviceAuth } = await userAgent.com.atproto.server.getServiceAuth(
    {
    aud: `did:web:${userAgent.dispatchUrl.host}`,
    lxm: "com.atproto.repo.uploadBlob",
    exp: Date.now() / 1000 + 60 * 30, // 30 minutes
    },
    );

    const token = serviceAuth.token;

    const file = await Deno.open(videoPath);
    const { size } = await file.stat();

    // optional: print upload progress
    let bytesUploaded = 0;
    const progressTrackingStream = new TransformStream({
    transform(chunk, controller) {
    controller.enqueue(chunk);
    bytesUploaded += chunk.byteLength;
    console.log(
    "upload progress:",
    Math.trunc(bytesUploaded / size * 100) + "%",
    );
    },
    flush() {
    console.log("upload complete ✨");
    },
    });

    const uploadUrl = new URL(
    "https://video.bsky.app/xrpc/app.bsky.video.uploadVideo",
    );
    uploadUrl.searchParams.append("did", userAgent.session!.did);
    uploadUrl.searchParams.append("name", videoPath.split("/").pop()!);

    const uploadResponse = await fetch(uploadUrl, {
    method: "POST",
    headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "video/mp4",
    "Content-Length": String(size),
    },
    body: file.readable.pipeThrough(progressTrackingStream),
    });

    const jobStatus = (await uploadResponse.json()) as AppBskyVideoDefs.JobStatus;

    console.log("JobId:", jobStatus.jobId);

    let blob: BlobRef | undefined = jobStatus.blob;

    const videoAgent = new AtpAgent({ service: "https://video.bsky.app" });

    while (!blob) {
    const { data: status } = await videoAgent.app.bsky.video.getJobStatus(
    { jobId: jobStatus.jobId },
    );
    console.log(
    "Status:",
    status.jobStatus.state,
    status.jobStatus.progress || "",
    );
    if (status.jobStatus.blob) {
    blob = status.jobStatus.blob;
    }
    // wait a second
    await new Promise((resolve) => setTimeout(resolve, 1000));
    }

    console.log("posting...");

    await userAgent.post({
    text: "This post should have a video attached",
    langs: ["en"],
    embed: {
    $type: "app.bsky.embed.video",
    video: blob,
    aspectRatio: await getAspectRatio(videoPath),
    } satisfies AppBskyEmbedVideo.Main,
    });

    console.log("done ✨");

    // bonus: get aspect ratio using ffprobe
    // in the browser, you can just put the video uri in a <video> element
    // and measure the dimensions once it loads. in React Native, the image picker
    // will give you the dimensions directly

    import { ffprobe } from "https://deno.land/x/[email protected]/ffprobe.ts";

    async function getAspectRatio(fileName: string) {
    const { streams } = await ffprobe(fileName, {});
    const videoSteam = streams.find((stream) => stream.codec_type === "video");
    return {
    width: videoSteam.width,
    height: videoSteam.height,
    };
    }