+17
-1
src/components/composer/workers/gif-conversion.ts
+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
+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
+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) {
+2
src/lib/preferences/account.ts
+2
src/lib/preferences/account.ts
+1
src/lib/states/session.tsx
+1
src/lib/states/session.tsx
+69
src/lib/utils/misc.ts
+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
+
};