at://samuel.bsky.team/li.plonk.paste/3lsj64hqfcs2d

Back to Collection

Record JSON

{
  "$type": "li.plonk.paste",
  "code": "import {\r\n  AppBskyEmbedVideo,\r\n  AppBskyVideoDefs,\r\n  AtpAgent,\r\n  BlobRef,\r\n} from \"npm:@atproto/api\";\r\n\r\nconst userAgent = new AtpAgent({\r\n  service: prompt(\"Service URL (default: https://bsky.social):\") ||\r\n    \"https://bsky.social\",\r\n});\r\n\r\nawait userAgent.login({\r\n  identifier: prompt(\"Handle:\")!,\r\n  password: prompt(\"Password:\")!,\r\n});\r\n\r\nconsole.log(`Logged in as ${userAgent.session?.handle}`);\r\n\r\nconst videoPath = prompt(\"Video file (.mp4):\")!;\r\n\r\nconst { data: serviceAuth } = await userAgent.com.atproto.server.getServiceAuth(\r\n  {\r\n    aud: `did:web:${userAgent.dispatchUrl.host}`,\r\n    lxm: \"com.atproto.repo.uploadBlob\",\r\n    exp: Date.now() / 1000 + 60 * 30, // 30 minutes\r\n  },\r\n);\r\n\r\nconst token = serviceAuth.token;\r\n\r\nconst file = await Deno.open(videoPath);\r\nconst { size } = await file.stat();\r\n\r\n// optional: print upload progress\r\nlet bytesUploaded = 0;\r\nconst progressTrackingStream = new TransformStream({\r\n  transform(chunk, controller) {\r\n    controller.enqueue(chunk);\r\n    bytesUploaded += chunk.byteLength;\r\n    console.log(\r\n      \"upload progress:\",\r\n      Math.trunc(bytesUploaded / size * 100) + \"%\",\r\n    );\r\n  },\r\n  flush() {\r\n    console.log(\"upload complete ✨\");\r\n  },\r\n});\r\n\r\nconst uploadUrl = new URL(\r\n  \"https://video.bsky.app/xrpc/app.bsky.video.uploadVideo\",\r\n);\r\nuploadUrl.searchParams.append(\"did\", userAgent.session!.did);\r\nuploadUrl.searchParams.append(\"name\", videoPath.split(\"/\").pop()!);\r\n\r\nconst uploadResponse = await fetch(uploadUrl, {\r\n  method: \"POST\",\r\n  headers: {\r\n    Authorization: `Bearer ${token}`,\r\n    \"Content-Type\": \"video/mp4\",\r\n    \"Content-Length\": String(size),\r\n  },\r\n  body: file.readable.pipeThrough(progressTrackingStream),\r\n});\r\n\r\nconst jobStatus = (await uploadResponse.json()) as AppBskyVideoDefs.JobStatus;\r\n\r\nconsole.log(\"JobId:\", jobStatus.jobId);\r\n\r\nlet blob: BlobRef | undefined = jobStatus.blob;\r\n\r\nconst videoAgent = new AtpAgent({ service: \"https://video.bsky.app\" });\r\n\r\nwhile (!blob) {\r\n  const { data: status } = await videoAgent.app.bsky.video.getJobStatus(\r\n    { jobId: jobStatus.jobId },\r\n  );\r\n  console.log(\r\n    \"Status:\",\r\n    status.jobStatus.state,\r\n    status.jobStatus.progress || \"\",\r\n  );\r\n  if (status.jobStatus.blob) {\r\n    blob = status.jobStatus.blob;\r\n  }\r\n  // wait a second\r\n  await new Promise((resolve) =\u003e setTimeout(resolve, 1000));\r\n}\r\n\r\nconsole.log(\"posting...\");\r\n\r\nawait userAgent.post({\r\n  text: \"This post should have a video attached\",\r\n  langs: [\"en\"],\r\n  embed: {\r\n    $type: \"app.bsky.embed.video\",\r\n    video: blob,\r\n    aspectRatio: await getAspectRatio(videoPath),\r\n  } satisfies AppBskyEmbedVideo.Main,\r\n});\r\n\r\nconsole.log(\"done ✨\");\r\n\r\n// bonus: get aspect ratio using ffprobe\r\n// in the browser, you can just put the video uri in a \u003cvideo\u003e element\r\n// and measure the dimensions once it loads. in React Native, the image picker\r\n// will give you the dimensions directly\r\n\r\nimport { ffprobe } from \"https://deno.land/x/fast_forward@0.1.6/ffprobe.ts\";\r\n\r\nasync function getAspectRatio(fileName: string) {\r\n  const { streams } = await ffprobe(fileName, {});\r\n  const videoSteam = streams.find((stream) =\u003e stream.codec_type === \"video\");\r\n  return {\r\n    width: videoSteam.width,\r\n    height: videoSteam.height,\r\n  };\r\n}",
  "createdAt": "2025-06-26T12:51:23.936Z",
  "lang": "typescript",
  "shortUrl": "PA",
  "title": "video-upload.ts"
}