Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at natb/hide-msg 262 lines 7.1 kB view raw
1import { AppBskyFeedPost, BlobRef, RichText } from "@atproto/api"; 2import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 3import { StreamplaceAgent } from "streamplace/src/agent"; 4import { PlaceStreamLivestream } from "streamplace/src/lexicons"; 5import { LivestreamViewHydrated } from "streamplace/src/useful-types"; 6import { useUrl } from "./streamplace-store"; 7import { usePDSAgent } from "./xrpc"; 8 9const uploadThumbnail = async ( 10 pdsAgent: StreamplaceAgent, 11 customThumbnail?: Blob, 12) => { 13 if (customThumbnail) { 14 let tries = 0; 15 try { 16 let thumbnail = await pdsAgent.uploadBlob(customThumbnail); 17 18 while ( 19 thumbnail.data.blob.size === 0 && 20 customThumbnail.size !== 0 && 21 tries < 3 22 ) { 23 console.warn( 24 "Reuploading blob as blob sizes don't match! Blob size recieved is", 25 thumbnail.data.blob.size, 26 "and sent blob size is", 27 customThumbnail.size, 28 ); 29 thumbnail = await pdsAgent.uploadBlob(customThumbnail); 30 } 31 32 if (tries === 3) { 33 throw new Error("Could not successfully upload blob (tried thrice)"); 34 } 35 36 if (thumbnail.success) { 37 console.log("Successfully uploaded thumbnail"); 38 return thumbnail.data.blob; 39 } 40 } catch (e) { 41 throw new Error("Error uploading thumbnail: " + e); 42 } 43 } 44}; 45 46async function createNewPost( 47 agent: StreamplaceAgent, 48 record: AppBskyFeedPost.Record, 49): Promise<{ uri: string; cid: string }> { 50 try { 51 const post = await agent.post(record); 52 53 return { uri: post.uri, cid: post.cid }; 54 } catch (error) { 55 console.error("Error creating new post:", error); 56 throw error; 57 } 58} 59 60function buildGoLivePost( 61 text: string, 62 url: URL, 63 profile: ProfileViewDetailed, 64 params: URLSearchParams, 65 thumbnail: BlobRef | undefined, 66): AppBskyFeedPost.Record { 67 const now = new Date(); 68 const linkUrl = `${url.protocol}//${url.host}/${profile.handle}?${params.toString()}`; 69 const prefix = `🔴 LIVE `; 70 const textUrl = `${url.protocol}//${url.host}/${profile.handle}`; 71 const suffix = ` ${text}`; 72 const content = prefix + textUrl + suffix; 73 74 const rt = new RichText({ text: content }); 75 rt.detectFacetsWithoutResolution(); 76 const record: AppBskyFeedPost.Record = { 77 $type: "app.bsky.feed.post", 78 text: content, 79 "place.stream.livestream": { 80 url: linkUrl, 81 title: text, 82 }, 83 facets: rt.facets, 84 createdAt: now.toISOString(), 85 }; 86 record.embed = { 87 $type: "app.bsky.embed.external", 88 external: { 89 description: text, 90 thumb: thumbnail, 91 title: `@${profile.handle} is 🔴LIVE on ${url.host}!`, 92 uri: linkUrl, 93 }, 94 }; 95 96 return record; 97} 98 99export function useCreateStreamRecord() { 100 let agent = usePDSAgent(); 101 let url = useUrl(); 102 103 return async ( 104 title: string, 105 customThumbnail?: Blob, 106 submitPost: boolean = true, 107 ) => { 108 if (!agent) { 109 throw new Error("No PDS agent found"); 110 } 111 112 if (!agent.did) { 113 throw new Error("No user DID found, assuming not logged in"); 114 } 115 116 let thumbnail: BlobRef | undefined = undefined; 117 118 const u = new URL(url); 119 120 if (customThumbnail) { 121 try { 122 thumbnail = await uploadThumbnail(agent, customThumbnail); 123 } catch (e) { 124 throw new Error(`Custom thumbnail upload failed ${e}`); 125 } 126 } else { 127 // No custom thumbnail: fetch the server-side image and upload it 128 // try thrice lel 129 let tries = 0; 130 try { 131 for (; tries < 3; tries++) { 132 try { 133 console.log( 134 `Fetching thumbnail from ${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`, 135 ); 136 const thumbnailRes = await fetch( 137 `${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`, 138 ); 139 if (!thumbnailRes.ok) { 140 throw new Error( 141 `Failed to fetch thumbnail: ${thumbnailRes.status})`, 142 ); 143 } 144 const thumbnailBlob = await thumbnailRes.blob(); 145 console.log(thumbnailBlob); 146 thumbnail = await uploadThumbnail(agent, thumbnailBlob); 147 } catch (e) { 148 console.warn( 149 `Failed to fetch thumbnail, retrying (${tries + 1}/3): ${e}`, 150 ); 151 // Wait 1 second before retrying 152 await new Promise((resolve) => setTimeout(resolve, 2000)); 153 if (tries === 2) { 154 throw new Error(`Failed to fetch thumbnail after 3 tries: ${e}`); 155 } 156 } 157 } 158 } catch (e) { 159 throw new Error(`Thumbnail upload failed ${e}`); 160 } 161 } 162 163 let newPost: undefined | { uri: string; cid: string } = undefined; 164 165 if (submitPost) { 166 const did = agent.did; 167 const profile = await agent.getProfile({ actor: did }); 168 169 if (!profile) { 170 throw new Error("No profile found for the user DID"); 171 } 172 173 const params = new URLSearchParams({ 174 did: did, 175 time: new Date().toISOString(), 176 }); 177 178 let post = buildGoLivePost(title, u, profile.data, params, thumbnail); 179 180 newPost = await createNewPost(agent, post); 181 182 if (!newPost.uri || !newPost.cid) { 183 throw new Error( 184 "Cannot read properties of undefined (reading 'uri' or 'cid')", 185 ); 186 } 187 } 188 189 const record: PlaceStreamLivestream.Record = { 190 title: title, 191 url: url, 192 createdAt: new Date().toISOString(), 193 post: newPost, 194 thumb: thumbnail, 195 }; 196 197 await agent.com.atproto.repo.createRecord({ 198 repo: agent.did, 199 collection: "place.stream.livestream", 200 record, 201 }); 202 return record; 203 }; 204} 205 206export function useUpdateStreamRecord() { 207 let agent = usePDSAgent(); 208 let url = useUrl(); 209 210 return async ( 211 title: string, 212 livestream: LivestreamViewHydrated | null, 213 customThumbnail?: Blob, 214 ) => { 215 if (!agent) { 216 throw new Error("No PDS agent found"); 217 } 218 219 if (!agent.did) { 220 throw new Error("No user DID found, assuming not logged in"); 221 } 222 223 if (!livestream) { 224 throw new Error("No latest record"); 225 } 226 227 let rkey = livestream.uri.split("/").pop(); 228 let oldRecordValue: PlaceStreamLivestream.Record = livestream.record; 229 230 if (!rkey) { 231 throw new Error("No rkey?"); 232 } 233 234 let thumbnail: BlobRef | undefined = oldRecordValue.thumb; 235 236 // update thumbnail if a new one is provided 237 if (customThumbnail) { 238 try { 239 thumbnail = await uploadThumbnail(agent, customThumbnail); 240 } catch (e) { 241 throw new Error(`Custom thumbnail upload failed ${e}`); 242 } 243 } 244 245 const record: PlaceStreamLivestream.Record = { 246 title: title, 247 url: url, 248 createdAt: new Date().toISOString(), 249 post: oldRecordValue.post, 250 thumb: thumbnail, 251 }; 252 253 await agent.com.atproto.repo.putRecord({ 254 repo: agent.did, 255 collection: "place.stream.livestream", 256 rkey, 257 record, 258 }); 259 260 return record; 261 }; 262}