Skip to content

Commit

Permalink
[Video] Add uploaded video to post (#4884)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
mozzius and mozzius authored Aug 29, 2024
1 parent d52d296 commit 551c4a4
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 126 deletions.
38 changes: 27 additions & 11 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedPostgate,
AtUri,
BlobRef,
BskyAgent,
ComAtprotoLabelDefs,
RichText,
} from '@atproto/api'
import {AtUri} from '@atproto/api'

import {logger} from '#/logger'
import {writePostgateRecord} from '#/state/queries/postgate'
Expand Down Expand Up @@ -43,10 +45,7 @@ interface PostOpts {
uri: string
cid: string
}
video?: {
uri: string
cid: string
}
video?: BlobRef
extLink?: ExternalEmbedDraft
images?: ImageModel[]
labels?: string[]
Expand All @@ -61,18 +60,16 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
| AppBskyEmbedImages.Main
| AppBskyEmbedExternal.Main
| AppBskyEmbedRecord.Main
| AppBskyEmbedVideo.Main
| AppBskyEmbedRecordWithMedia.Main
| undefined
let reply
let rt = new RichText(
{text: opts.rawText.trimEnd()},
{
cleanNewlines: true,
},
)
let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true})

opts.onStateChange?.('Processing...')

await rt.detectFacets(agent)

rt = shortenLinks(rt)
rt = stripInvalidMentions(rt)

Expand Down Expand Up @@ -129,6 +126,25 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
}
}

// add video embed if present
if (opts.video) {
if (opts.quote) {
embed = {
$type: 'app.bsky.embed.recordWithMedia',
record: embed,
media: {
$type: 'app.bsky.embed.video',
video: opts.video,
} as AppBskyEmbedVideo.Main,
} as AppBskyEmbedRecordWithMedia.Main
} else {
embed = {
$type: 'app.bsky.embed.video',
video: opts.video,
} as AppBskyEmbedVideo.Main
}
}

// add external embed if present
if (opts.extLink && !opts.images?.length) {
if (opts.extLink.embed) {
Expand Down
36 changes: 0 additions & 36 deletions src/lib/media/video/types.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/state/queries/video/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {useMemo} from 'react'
import {AtpAgent} from '@atproto/api'

const UPLOAD_ENDPOINT = process.env.EXPO_PUBLIC_VIDEO_ROOT_ENDPOINT ?? ''

export const createVideoEndpointUrl = (
Expand All @@ -13,3 +16,11 @@ export const createVideoEndpointUrl = (
}
return url.href
}

export function useVideoAgent() {
return useMemo(() => {
return new AtpAgent({
service: UPLOAD_ENDPOINT,
})
}, [])
}
23 changes: 9 additions & 14 deletions src/state/queries/video/video-upload.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
import {AppBskyVideoDefs} from '@atproto/api'
import {useMutation} from '@tanstack/react-query'
import {nanoid} from 'nanoid/non-secure'

import {CompressedVideo} from '#/lib/media/video/compress'
import {UploadVideoResponse} from '#/lib/media/video/types'
import {createVideoEndpointUrl} from '#/state/queries/video/util'
import {useAgent, useSession} from '#/state/session'

const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''

export const useUploadVideoMutation = ({
onSuccess,
onError,
setProgress,
}: {
onSuccess: (response: UploadVideoResponse) => void
onSuccess: (response: AppBskyVideoDefs.JobStatus) => void
onError: (e: any) => void
setProgress: (progress: number) => void
}) => {
Expand All @@ -23,7 +21,7 @@ export const useUploadVideoMutation = ({

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

const {data: serviceAuth} =
await agent.api.com.atproto.server.getServiceAuth({
const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth(
{
aud: `did:web:${agent.pdsUrl.hostname}`,
lxm: 'com.atproto.repo.uploadBlob',
})
},
)

const uploadTask = createUploadTask(
uri,
video.uri,
{
headers: {
'dev-key': UPLOAD_HEADER,
'content-type': 'video/mp4', // @TODO same question here. does the compression step always output mp4?
'content-type': 'video/mp4',
Authorization: `Bearer ${serviceAuth.token}`,
},
httpMethod: 'POST',
Expand All @@ -59,10 +57,7 @@ export const useUploadVideoMutation = ({
throw new Error('No response')
}

// @TODO rm, useful for debugging/getting video cid
console.log('[VIDEO]', res.body)
const responseBody = JSON.parse(res.body) as UploadVideoResponse
onSuccess(responseBody)
const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus
return responseBody
},
onError,
Expand Down
69 changes: 33 additions & 36 deletions src/state/queries/video/video-upload.web.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import {AppBskyVideoDefs} from '@atproto/api'
import {useMutation} from '@tanstack/react-query'
import {nanoid} from 'nanoid/non-secure'

import {CompressedVideo} from '#/lib/media/video/compress'
import {UploadVideoResponse} from '#/lib/media/video/types'
import {createVideoEndpointUrl} from '#/state/queries/video/util'
import {useAgent, useSession} from '#/state/session'

const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''

export const useUploadVideoMutation = ({
onSuccess,
onError,
setProgress,
}: {
onSuccess: (response: UploadVideoResponse) => void
onSuccess: (response: AppBskyVideoDefs.JobStatus) => void
onError: (e: any) => void
setProgress: (progress: number) => void
}) => {
Expand All @@ -22,56 +20,55 @@ export const useUploadVideoMutation = ({

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

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

const {data: serviceAuth} =
await agent.api.com.atproto.server.getServiceAuth({
const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth(
{
aud: `did:web:${agent.pdsUrl.hostname}`,
lxm: 'com.atproto.repo.uploadBlob',
})
},
)

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

const xhr = new XMLHttpRequest()
const res = (await new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', e => {
const progress = e.loaded / e.total
setProgress(progress)
})
xhr.onloadend = () => {
if (xhr.readyState === 4) {
const uploadRes = JSON.parse(
xhr.responseText,
) as UploadVideoResponse
resolve(uploadRes)
onSuccess(uploadRes)
} else {
const res = await new Promise<AppBskyVideoDefs.JobStatus>(
(resolve, reject) => {
xhr.upload.addEventListener('progress', e => {
const progress = e.loaded / e.total
setProgress(progress)
})
xhr.onloadend = () => {
if (xhr.readyState === 4) {
const uploadRes = JSON.parse(
xhr.responseText,
) as AppBskyVideoDefs.JobStatus
resolve(uploadRes)
onSuccess(uploadRes)
} else {
reject()
onError(new Error('Failed to upload video'))
}
}
xhr.onerror = () => {
reject()
onError(new Error('Failed to upload video'))
}
}
xhr.onerror = () => {
reject()
onError(new Error('Failed to upload video'))
}
xhr.open('POST', uri)
xhr.setRequestHeader('Content-Type', 'video/mp4') // @TODO how we we set the proper content type?
// @TODO remove this header for prod
xhr.setRequestHeader('dev-key', UPLOAD_HEADER)
xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`)
xhr.send(bytes)
})) as UploadVideoResponse
xhr.open('POST', uri)
xhr.setRequestHeader('Content-Type', 'video/mp4')
xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`)
xhr.send(bytes)
},
)

// @TODO rm for prod
console.log('[VIDEO]', res)
return res
},
onError,
Expand Down
Loading

0 comments on commit 551c4a4

Please sign in to comment.