personal web client for Bluesky
typescript solidjs bluesky atcute

Compare changes

Choose any two refs to compare.

Changed files
+121 -5
src
components
composer
embeds
lib
navigation
preferences
states
utils
+17 -1
src/components/composer/workers/gif-conversion.ts
··· 13 13 throw new Error(`GIF has no frames`); 14 14 } 15 15 16 + if (frameCount === 1) { 17 + const { image } = await decoder.decode({ frameIndex: 0 }); 18 + const canvas = new OffscreenCanvas(image.displayWidth, image.displayHeight); 19 + const ctx = canvas.getContext('2d')!; 20 + ctx.drawImage(image, 0, 0); 21 + return await canvas.convertToBlob({ type: 'image/png' }); 22 + } 23 + 16 24 let output: Output<WebMOutputFormat, BufferTarget>; 17 25 let videoSource: VideoSampleSource; 18 26 19 27 { 20 28 const { image } = await decoder.decode({ frameIndex: 0 }); 29 + const { displayWidth, displayHeight } = image; 30 + 31 + // Scale bitrate based on resolution (~5 Mbps at 1080p, sqrt curve for smaller sizes) 32 + const pixels = displayWidth * displayHeight; 33 + const bitrate = Math.max( 34 + 500_000, 35 + Math.min(8_000_000, Math.round(Math.sqrt(pixels / (1920 * 1080)) * 5_000_000)), 36 + ); 21 37 22 38 output = new Output({ 23 39 format: new WebMOutputFormat(), 24 40 target: new BufferTarget(), 25 41 }); 26 42 27 - videoSource = new VideoSampleSource({ codec: 'vp9', bitrate: 1e6 }); 43 + videoSource = new VideoSampleSource({ codec: 'vp9', bitrate }); 28 44 output.addVideoTrack(videoSource); 29 45 30 46 await output.start();
+22 -1
src/components/embeds/players/video-player.tsx
··· 8 8 9 9 import { replaceVideoCdnUrl } from '~/lib/bsky/video'; 10 10 import { useSession } from '~/lib/states/session'; 11 + import { throttleTrailing } from '~/lib/utils/misc'; 11 12 12 13 const isMobile = /Android|iPhone|iPad|iPod/.test(navigator.userAgent); 13 14 ··· 21 22 22 23 const [playing, setPlaying] = createSignal(false); 23 24 25 + // const bwEstimate = currentAccount?.preferences.ui.videoBwEstimate; 26 + const bwEstimate = undefined; 24 27 const hls = new Hls({ 25 28 capLevelToPlayerSize: true, 26 - startLevel: 1, 29 + 30 + // the '-1' value makes a test request to estimate bandwidth and quality level 31 + // before showing the first fragment 32 + startLevel: bwEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel, 33 + 27 34 xhrSetup(xhr, urlString) { 28 35 // We want to replace the URL here so it points directly to the CDN, 29 36 // and not the middleware service. ··· 46 53 }, 47 54 }); 48 55 56 + if (bwEstimate !== undefined) { 57 + hls.bandwidthEstimate = bwEstimate; 58 + } 59 + 49 60 onCleanup(() => hls.destroy()); 50 61 51 62 hls.loadSource(embed.playlist); ··· 60 71 if (!isMobile && currentAccount) { 61 72 node.volume = currentAccount.preferences.ui.mediaVolume; 62 73 } 74 + 75 + hls.on( 76 + Hls.Events.FRAG_LOADED, 77 + throttleTrailing(() => { 78 + if (currentAccount && !Number.isNaN(hls.bandwidthEstimate)) { 79 + currentAccount.preferences.ui.videoBwEstimate = 80 + Math.round(hls.bandwidthEstimate / 1_000_000) * 1_000_000; 81 + } 82 + }, 5_000), 83 + ); 63 84 64 85 hls.on(Hls.Events.LEVEL_LOADED, (_event, data) => { 65 86 const hasAudio = data.levelInfo.audioCodec !== undefined;
+5 -1
src/components/rich-text.tsx
··· 4 4 import type { AppBskyRichtextFacet } from '@atcute/bluesky'; 5 5 import { segmentize } from '@atcute/bluesky-richtext-segmenter'; 6 6 7 - import { isLinkValid } from '~/api/utils/strings'; 7 + import { isLinkValid, safeUrlParse } from '~/api/utils/strings'; 8 8 9 9 import { getCdnUrl } from '~/lib/bluemoji/render'; 10 10 import { redirectBskyUrl } from '~/lib/redirector'; ··· 46 46 47 47 if (type === 'app.bsky.richtext.facet#link') { 48 48 const uri = feature.uri; 49 + if (safeUrlParse(uri) === null) { 50 + break; 51 + } 52 + 49 53 const redirect = redirectBskyUrl(uri); 50 54 51 55 if (redirect == null) {
+5 -2
src/lib/navigation/router.tsx
··· 305 305 }; 306 306 307 307 export const useIsFocused = (): Accessor<boolean> => { 308 - const { isActive } = useViewContext(); 308 + const context = useViewContext(); 309 + if (context === undefined) { 310 + return () => true; 311 + } 309 312 310 - return isActive; 313 + return context.isActive; 311 314 }; 312 315 313 316 export const createFocusEffect = (cb: () => void) => {
+2
src/lib/preferences/account.ts
··· 14 14 } 15 15 16 16 export interface UIPreferences { 17 + /** Media bandwidth estimate */ 18 + videoBwEstimate: number | undefined; 17 19 /** Media player volume */ 18 20 mediaVolume: number; 19 21 }
+1
src/lib/states/session.tsx
··· 288 288 }, 289 289 ], 290 290 ui: { 291 + videoBwEstimate: undefined, 291 292 mediaVolume: 0.25, 292 293 }, 293 294 composer: {
+69
src/lib/utils/misc.ts
··· 88 88 89 89 return result as Omit<T, K>; 90 90 }; 91 + 92 + export const throttleLeading = <T extends (...args: any[]) => void>( 93 + fn: T, 94 + wait: number, 95 + ): ((...args: Parameters<T>) => void) => { 96 + let lastCallTime: number | undefined; 97 + 98 + return (...args: Parameters<T>) => { 99 + const now = performance.now(); 100 + 101 + if (lastCallTime === undefined || now - lastCallTime >= wait) { 102 + lastCallTime = now; 103 + fn(...args); 104 + } 105 + }; 106 + }; 107 + 108 + export const throttleTrailing = <T extends (...args: any[]) => void>( 109 + fn: T, 110 + wait: number, 111 + ): ((...args: Parameters<T>) => void) => { 112 + let timeoutId: ReturnType<typeof setTimeout> | undefined; 113 + let lastArgs: Parameters<T> | undefined; 114 + 115 + return (...args: Parameters<T>) => { 116 + lastArgs = args; 117 + 118 + if (timeoutId === undefined) { 119 + timeoutId = setTimeout(() => { 120 + timeoutId = undefined; 121 + fn(...lastArgs!); 122 + }, wait); 123 + } 124 + }; 125 + }; 126 + 127 + export const throttle = <T extends (...args: any[]) => void>( 128 + fn: T, 129 + wait: number, 130 + ): ((...args: Parameters<T>) => void) => { 131 + let timeoutId: ReturnType<typeof setTimeout> | undefined; 132 + let lastArgs: Parameters<T> | undefined; 133 + let lastCallTime: number | undefined; 134 + 135 + return (...args: Parameters<T>) => { 136 + const now = performance.now(); 137 + const elapsed = lastCallTime !== undefined ? now - lastCallTime : wait; 138 + 139 + if (elapsed >= wait) { 140 + if (timeoutId !== undefined) { 141 + clearTimeout(timeoutId); 142 + timeoutId = undefined; 143 + } 144 + 145 + lastCallTime = now; 146 + fn(...args); 147 + } else { 148 + lastArgs = args; 149 + 150 + if (timeoutId === undefined) { 151 + timeoutId = setTimeout(() => { 152 + timeoutId = undefined; 153 + lastCallTime = performance.now(); 154 + fn(...lastArgs!); 155 + }, wait - elapsed); 156 + } 157 + } 158 + }; 159 + };