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,
};
}
@Durss
Copy link

Durss commented Mar 21, 2025

If that can be of any help to anyone, here is an equivalent-ish NodeJS function that uploads the video and returns an embed object (i stripped out the auth/post parts that i handle externally)
I also stripped out the aspect ratio part because I was to lazy to convert it to NodeJS.

import {
	AppBskyEmbedVideo,
	AppBskyVideoDefs,
	AtpAgent,
	BlobRef,
} from "@atproto/api";

export async function uploadVideo(agent:AtpAgent, videoPath:string):Promise<any> {
	const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth(
		{
			aud: `did:web:${agent.dispatchUrl.host}`,
			lxm: "com.atproto.repo.uploadBlob",
			exp: Date.now() / 1000 + 60 * 30, // 30 minutes
		},
	);

    async function downloadVideo(url: string): Promise<{ video: Buffer, size: number }> {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`Failed to fetch video: ${response.statusText}`);
        }
        const arrayBuffer = await response.arrayBuffer();
        const video = Buffer.from(arrayBuffer);
        const size = video.length;
        return { video, size };
    }

	const video = await downloadVideo(videoPath);

	console.log("Downloaded video", videoPath, video.size);
	
	const uploadUrl = new URL("https://video.bsky.app/xrpc/app.bsky.video.uploadVideo");
	uploadUrl.searchParams.append("did", agent.session!.did);
	uploadUrl.searchParams.append("name", videoPath.split("/").pop()!);
	
	const uploadResponse = await fetch(uploadUrl, {
		method: "POST",
		headers: {
			Authorization: `Bearer ${serviceAuth.token}`,
			"Content-Type": "video/mp4",
			"Content-Length": video.size.toString(),
		},
		body: video.video
	});
	
	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...");

	return {
		$type: "app.bsky.embed.video",
		video: blob,
	} satisfies AppBskyEmbedVideo.Main
}

@GrantCuster
Copy link

Thanks @Durss this just helped me out!

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