Something went wrong. Try again.
upload a video to bluesky. the video is pre-processed by uploading to the video service before making the post
Something went wrong. Try again.
3.2 kB · 121 lines · Created 1y ago
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122// NOTE: this uses Deno
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 progresslet 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/fast_forward@0.1.6/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, };}