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.
Bluesky video upload - direct upload
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,
};
}
@minter
Copy link

minter commented Nov 12, 2024

@mozzius I'm attempting to implement the happy path in Ruby, based on this example. Everything seems to be working, except that when I try to attach the video blob to the post, it's giving me a "Blob not found" error. I'm not really sure what could be happening. The blob JSON looks visually correct.

This may have an easy answer, or I'm glad to post some example code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment