home · login to get plonkin'

video-upload.ts

@samuel.bsky.team · 10d ago · typescript · 119 loc · raw · 0 comments

1import {2  AppBskyEmbedVideo,3  AppBskyVideoDefs,4  AtpAgent,5  BlobRef,6} from "npm:@atproto/api";78const userAgent = new AtpAgent({9  service: prompt("Service URL (default: https://bsky.social):") ||10    "https://bsky.social",11});1213await userAgent.login({14  identifier: prompt("Handle:")!,15  password: prompt("Password:")!,16});1718console.log(`Logged in as ${userAgent.session?.handle}`);1920const videoPath = prompt("Video file (.mp4):")!;2122const { data: serviceAuth } = await userAgent.com.atproto.server.getServiceAuth(23  {24    aud: `did:web:${userAgent.dispatchUrl.host}`,25    lxm: "com.atproto.repo.uploadBlob",26    exp: Date.now() / 1000 + 60 * 30, // 30 minutes27  },28);2930const token = serviceAuth.token;3132const file = await Deno.open(videoPath);33const { size } = await file.stat();3435// optional: print upload progress36let bytesUploaded = 0;37const progressTrackingStream = new TransformStream({38  transform(chunk, controller) {39    controller.enqueue(chunk);40    bytesUploaded += chunk.byteLength;41    console.log(42      "upload progress:",43      Math.trunc(bytesUploaded / size * 100) + "%",44    );45  },46  flush() {47    console.log("upload complete ✨");48  },49});5051const uploadUrl = new URL(52  "https://video.bsky.app/xrpc/app.bsky.video.uploadVideo",53);54uploadUrl.searchParams.append("did", userAgent.session!.did);55uploadUrl.searchParams.append("name", videoPath.split("/").pop()!);5657const uploadResponse = await fetch(uploadUrl, {58  method: "POST",59  headers: {60    Authorization: `Bearer ${token}`,61    "Content-Type": "video/mp4",62    "Content-Length": String(size),63  },64  body: file.readable.pipeThrough(progressTrackingStream),65});6667const jobStatus = (await uploadResponse.json()) as AppBskyVideoDefs.JobStatus;6869console.log("JobId:", jobStatus.jobId);7071let blob: BlobRef | undefined = jobStatus.blob;7273const videoAgent = new AtpAgent({ service: "https://video.bsky.app" });7475while (!blob) {76  const { data: status } = await videoAgent.app.bsky.video.getJobStatus(77    { jobId: jobStatus.jobId },78  );79  console.log(80    "Status:",81    status.jobStatus.state,82    status.jobStatus.progress || "",83  );84  if (status.jobStatus.blob) {85    blob = status.jobStatus.blob;86  }87  // wait a second88  await new Promise((resolve) => setTimeout(resolve, 1000));89}9091console.log("posting...");9293await userAgent.post({94  text: "This post should have a video attached",95  langs: ["en"],96  embed: {97    $type: "app.bsky.embed.video",98    video: blob,99    aspectRatio: await getAspectRatio(videoPath),100  } satisfies AppBskyEmbedVideo.Main,101});102103console.log("done ✨");104105// bonus: get aspect ratio using ffprobe106// in the browser, you can just put the video uri in a <video> element107// and measure the dimensions once it loads. in React Native, the image picker108// will give you the dimensions directly109110import { ffprobe } from "https://deno.land/x/fast_forward@0.1.6/ffprobe.ts";111112async function getAspectRatio(fileName: string) {113  const { streams } = await ffprobe(fileName, {});114  const videoSteam = streams.find((stream) => stream.codec_type === "video");115  return {116    width: videoSteam.width,117    height: videoSteam.height,118  };119}

login to post a comment