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"
}