Skip to content

Commit 551c4a4

Browse files
authored
[Video] Add uploaded video to post (#4884)
* video uploads! * use video upload lexicons * add missing postgate * remove references to prerelease package * fix scrubber showing a "0" * Delete types.ts * rm logs * rm upload header --------- Co-authored-by: Samuel Newman <[email protected]>
1 parent d52d296 commit 551c4a4

File tree

8 files changed

+116
-126
lines changed

8 files changed

+116
-126
lines changed

src/lib/api/index.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import {
33
AppBskyEmbedImages,
44
AppBskyEmbedRecord,
55
AppBskyEmbedRecordWithMedia,
6+
AppBskyEmbedVideo,
67
AppBskyFeedPostgate,
8+
AtUri,
9+
BlobRef,
710
BskyAgent,
811
ComAtprotoLabelDefs,
912
RichText,
1013
} from '@atproto/api'
11-
import {AtUri} from '@atproto/api'
1214

1315
import {logger} from '#/logger'
1416
import {writePostgateRecord} from '#/state/queries/postgate'
@@ -43,10 +45,7 @@ interface PostOpts {
4345
uri: string
4446
cid: string
4547
}
46-
video?: {
47-
uri: string
48-
cid: string
49-
}
48+
video?: BlobRef
5049
extLink?: ExternalEmbedDraft
5150
images?: ImageModel[]
5251
labels?: string[]
@@ -61,18 +60,16 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
6160
| AppBskyEmbedImages.Main
6261
| AppBskyEmbedExternal.Main
6362
| AppBskyEmbedRecord.Main
63+
| AppBskyEmbedVideo.Main
6464
| AppBskyEmbedRecordWithMedia.Main
6565
| undefined
6666
let reply
67-
let rt = new RichText(
68-
{text: opts.rawText.trimEnd()},
69-
{
70-
cleanNewlines: true,
71-
},
72-
)
67+
let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true})
7368

7469
opts.onStateChange?.('Processing...')
70+
7571
await rt.detectFacets(agent)
72+
7673
rt = shortenLinks(rt)
7774
rt = stripInvalidMentions(rt)
7875

@@ -129,6 +126,25 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
129126
}
130127
}
131128

129+
// add video embed if present
130+
if (opts.video) {
131+
if (opts.quote) {
132+
embed = {
133+
$type: 'app.bsky.embed.recordWithMedia',
134+
record: embed,
135+
media: {
136+
$type: 'app.bsky.embed.video',
137+
video: opts.video,
138+
} as AppBskyEmbedVideo.Main,
139+
} as AppBskyEmbedRecordWithMedia.Main
140+
} else {
141+
embed = {
142+
$type: 'app.bsky.embed.video',
143+
video: opts.video,
144+
} as AppBskyEmbedVideo.Main
145+
}
146+
}
147+
132148
// add external embed if present
133149
if (opts.extLink && !opts.images?.length) {
134150
if (opts.extLink.embed) {

src/lib/media/video/types.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/state/queries/video/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import {useMemo} from 'react'
2+
import {AtpAgent} from '@atproto/api'
3+
14
const UPLOAD_ENDPOINT = process.env.EXPO_PUBLIC_VIDEO_ROOT_ENDPOINT ?? ''
25

36
export const createVideoEndpointUrl = (
@@ -13,3 +16,11 @@ export const createVideoEndpointUrl = (
1316
}
1417
return url.href
1518
}
19+
20+
export function useVideoAgent() {
21+
return useMemo(() => {
22+
return new AtpAgent({
23+
service: UPLOAD_ENDPOINT,
24+
})
25+
}, [])
26+
}

src/state/queries/video/video-upload.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
2+
import {AppBskyVideoDefs} from '@atproto/api'
23
import {useMutation} from '@tanstack/react-query'
34
import {nanoid} from 'nanoid/non-secure'
45

56
import {CompressedVideo} from '#/lib/media/video/compress'
6-
import {UploadVideoResponse} from '#/lib/media/video/types'
77
import {createVideoEndpointUrl} from '#/state/queries/video/util'
88
import {useAgent, useSession} from '#/state/session'
99

10-
const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''
11-
1210
export const useUploadVideoMutation = ({
1311
onSuccess,
1412
onError,
1513
setProgress,
1614
}: {
17-
onSuccess: (response: UploadVideoResponse) => void
15+
onSuccess: (response: AppBskyVideoDefs.JobStatus) => void
1816
onError: (e: any) => void
1917
setProgress: (progress: number) => void
2018
}) => {
@@ -23,7 +21,7 @@ export const useUploadVideoMutation = ({
2321

2422
return useMutation({
2523
mutationFn: async (video: CompressedVideo) => {
26-
const uri = createVideoEndpointUrl('/upload', {
24+
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
2725
did: currentAccount!.did,
2826
name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to?
2927
})
@@ -33,19 +31,19 @@ export const useUploadVideoMutation = ({
3331
throw new Error('Agent does not have a PDS URL')
3432
}
3533

36-
const {data: serviceAuth} =
37-
await agent.api.com.atproto.server.getServiceAuth({
34+
const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth(
35+
{
3836
aud: `did:web:${agent.pdsUrl.hostname}`,
3937
lxm: 'com.atproto.repo.uploadBlob',
40-
})
38+
},
39+
)
4140

4241
const uploadTask = createUploadTask(
4342
uri,
4443
video.uri,
4544
{
4645
headers: {
47-
'dev-key': UPLOAD_HEADER,
48-
'content-type': 'video/mp4', // @TODO same question here. does the compression step always output mp4?
46+
'content-type': 'video/mp4',
4947
Authorization: `Bearer ${serviceAuth.token}`,
5048
},
5149
httpMethod: 'POST',
@@ -59,10 +57,7 @@ export const useUploadVideoMutation = ({
5957
throw new Error('No response')
6058
}
6159

62-
// @TODO rm, useful for debugging/getting video cid
63-
console.log('[VIDEO]', res.body)
64-
const responseBody = JSON.parse(res.body) as UploadVideoResponse
65-
onSuccess(responseBody)
60+
const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus
6661
return responseBody
6762
},
6863
onError,

src/state/queries/video/video-upload.web.ts

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1+
import {AppBskyVideoDefs} from '@atproto/api'
12
import {useMutation} from '@tanstack/react-query'
23
import {nanoid} from 'nanoid/non-secure'
34

45
import {CompressedVideo} from '#/lib/media/video/compress'
5-
import {UploadVideoResponse} from '#/lib/media/video/types'
66
import {createVideoEndpointUrl} from '#/state/queries/video/util'
77
import {useAgent, useSession} from '#/state/session'
88

9-
const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''
10-
119
export const useUploadVideoMutation = ({
1210
onSuccess,
1311
onError,
1412
setProgress,
1513
}: {
16-
onSuccess: (response: UploadVideoResponse) => void
14+
onSuccess: (response: AppBskyVideoDefs.JobStatus) => void
1715
onError: (e: any) => void
1816
setProgress: (progress: number) => void
1917
}) => {
@@ -22,56 +20,55 @@ export const useUploadVideoMutation = ({
2220

2321
return useMutation({
2422
mutationFn: async (video: CompressedVideo) => {
25-
const uri = createVideoEndpointUrl('/upload', {
23+
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
2624
did: currentAccount!.did,
27-
name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to?
25+
name: `${nanoid(12)}.mp4`, // @TODO: make sure it's always mp4'
2826
})
2927

3028
// a logged-in agent should have this set, but we'll check just in case
3129
if (!agent.pdsUrl) {
3230
throw new Error('Agent does not have a PDS URL')
3331
}
3432

35-
const {data: serviceAuth} =
36-
await agent.api.com.atproto.server.getServiceAuth({
33+
const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth(
34+
{
3735
aud: `did:web:${agent.pdsUrl.hostname}`,
3836
lxm: 'com.atproto.repo.uploadBlob',
39-
})
37+
},
38+
)
4039

4140
const bytes = await fetch(video.uri).then(res => res.arrayBuffer())
4241

4342
const xhr = new XMLHttpRequest()
44-
const res = (await new Promise((resolve, reject) => {
45-
xhr.upload.addEventListener('progress', e => {
46-
const progress = e.loaded / e.total
47-
setProgress(progress)
48-
})
49-
xhr.onloadend = () => {
50-
if (xhr.readyState === 4) {
51-
const uploadRes = JSON.parse(
52-
xhr.responseText,
53-
) as UploadVideoResponse
54-
resolve(uploadRes)
55-
onSuccess(uploadRes)
56-
} else {
43+
const res = await new Promise<AppBskyVideoDefs.JobStatus>(
44+
(resolve, reject) => {
45+
xhr.upload.addEventListener('progress', e => {
46+
const progress = e.loaded / e.total
47+
setProgress(progress)
48+
})
49+
xhr.onloadend = () => {
50+
if (xhr.readyState === 4) {
51+
const uploadRes = JSON.parse(
52+
xhr.responseText,
53+
) as AppBskyVideoDefs.JobStatus
54+
resolve(uploadRes)
55+
onSuccess(uploadRes)
56+
} else {
57+
reject()
58+
onError(new Error('Failed to upload video'))
59+
}
60+
}
61+
xhr.onerror = () => {
5762
reject()
5863
onError(new Error('Failed to upload video'))
5964
}
60-
}
61-
xhr.onerror = () => {
62-
reject()
63-
onError(new Error('Failed to upload video'))
64-
}
65-
xhr.open('POST', uri)
66-
xhr.setRequestHeader('Content-Type', 'video/mp4') // @TODO how we we set the proper content type?
67-
// @TODO remove this header for prod
68-
xhr.setRequestHeader('dev-key', UPLOAD_HEADER)
69-
xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`)
70-
xhr.send(bytes)
71-
})) as UploadVideoResponse
65+
xhr.open('POST', uri)
66+
xhr.setRequestHeader('Content-Type', 'video/mp4')
67+
xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`)
68+
xhr.send(bytes)
69+
},
70+
)
7271

73-
// @TODO rm for prod
74-
console.log('[VIDEO]', res)
7572
return res
7673
},
7774
onError,

0 commit comments

Comments
 (0)