---
sidebar_position: 13
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Uploading Video
Video uploads differ slightly from image uploads, since they are typically much larger files and have a significantly long processing time.
The Bluesky video CDN also applies additional account-level limits on publishing videos. Bluesky-hosted accounts need to have verified their account email before uploading video, and there are limits on the number of video posts which can be posted per day. Apps can check email verification status using the `com.atproto.server.getSession` endpoint on the PDS.
## Simple method
The easiest way to upload a video is to upload it as you would an image - use `uploadBlob` to upload the file directly to the PDS, and reference the returned blob.
```typescript title="uploadBlob"
const { data } = await userAgent.com.atproto.repo.uploadBlob(
fs.readFileSync(videoPath),
);
await agent.post({
text: "This post should have a video attached",
langs: ["en"],
embed: {
$type: "app.bsky.embed.video",
video: data.blob,
aspectRatio: await getAspectRatio(videoPath),
} satisfies AppBskyEmbedVideo.Main,
});
```
However, this has the significant downside that the video only starts processing after the post is submitted - the video service only knows about the video once the post appears in the firehose. This means that people will be able to see the post before processing is complete, which will show a missing video for several seconds.
Instead, we can send the video directly to the video service for preprocessing.
## Recommended method
This is a little more involved, and involves communicating directly with the video service. The steps are:
1. Create a service token with an audience of your PDS, a scope allowing `uploadBlob`, and a slightly longer expiry - 30 minutes is recommended
2. Upload the video directly to `https://video.bsky.app` with the appropriate access token
3. Query `https://video.bsky.app` for the status of the processing job until it returns the BlobRef of the video
4. Use the BlobRef in your post
Here is a code snippet for the full flow:
```typescript title="uploadVideo"
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 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": fs.statSync(videoPath).size
},
body: fs.readFileSync(videoPath),
});
const jobStatus = (await uploadResponse.json()) as AppBskyVideoDefs.JobStatus;
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));
}
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,
});
```
What is happening behind the scenes is the video service runs the processing step on your video, then saves an optimised version of the video to your PDS on your behalf using the service token. It then returns the BlobRef it gets from your PDS to you via the `getJobStatus` API. Then, when you make your post, the video service already has the video ready immediately without a delay.
This method also allows for better UX, since you can show the processing state to the user and let them know if the processing job fails before they make the post.
There are complete code samples for both methods here (using Deno):
- [Simple Method](https://tangled.sh/strings/did:plc:p2cp5gopk7mgjegy6wadk3ep/3lw74be4dr422)
- [Video Service](https://tangled.sh/strings/did:plc:p2cp5gopk7mgjegy6wadk3ep/3lw743ejno722)
Note: If the source video has already been processed by the video service, `getJobStatus` will return an error with the message `already_exists`. When this happens, it will return the BlobRef of the previously processed video for you to use, hence why we recommend checking for the presence of a BlobRef in the response regardless of the success or failure of the job.
### Aspect Ratios
As with images, we need to give video embeds an aspect ratio. This metadata can be a little tricky to compute. If you’re in the browser, you can load the video into a `