feat: implement image queue service and audio preloading to prevent connection limits

This commit is contained in:
Tomáš Mládek 2025-07-27 20:43:34 +02:00
parent 5f410715ca
commit 0afb320728
3 changed files with 137 additions and 16 deletions

View file

@ -1,5 +1,5 @@
<template> <template>
<audio ref="audio" :src="definition.src" loop preload="auto" /> <audio ref="audio" :src="audioSrc" loop preload="auto" />
</template> </template>
<script lang="ts"> <script lang="ts">
@ -20,10 +20,38 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const audio = ref<HTMLAudioElement | null>(null); const audio = ref<HTMLAudioElement | null>(null);
const audioSrc = ref<string>(""); // Ref to hold audio source after preloading
const isPreloaded = ref<boolean>(false);
console.debug(`[AUDIOAREA] Initializing ${props.definition.src}...`); console.debug(`[AUDIOAREA] Initializing ${props.definition.src}...`);
console.debug(props.definition); console.debug(props.definition);
// Preload the audio file completely to avoid keeping connections open
const preloadAudio = async (src: string) => {
console.debug(`[AUDIOAREA] Preloading audio: ${src}`);
try {
// Fetch the entire audio file
const response = await fetch(src);
if (!response.ok) throw new Error(`Failed to load audio: ${response.statusText}`);
// Convert to blob to ensure full download
const blob = await response.blob();
// Create a blob URL to use as the audio source
const blobUrl = URL.createObjectURL(blob);
audioSrc.value = blobUrl;
isPreloaded.value = true;
console.debug(`[AUDIOAREA] Successfully preloaded audio: ${src}`);
} catch (error) {
console.error(`[AUDIOAREA] Error preloading audio: ${error}`);
// Fall back to original source if preloading fails
audioSrc.value = src;
}
};
// Start preloading when component is created
preloadAudio(props.definition.src);
const MIN_SCALE = 0.02; const MIN_SCALE = 0.02;
const MIN_VOLUME_MULTIPLIER = 0.33; const MIN_VOLUME_MULTIPLIER = 0.33;
const vol_x = (1 - MIN_VOLUME_MULTIPLIER) / (1 - MIN_SCALE); const vol_x = (1 - MIN_VOLUME_MULTIPLIER) / (1 - MIN_SCALE);
@ -61,6 +89,7 @@ export default defineComponent({
return { return {
audio, audio,
audioSrc,
}; };
}, },
}); });

View file

@ -31,6 +31,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { rotate } from "@/utils"; import { rotate } from "@/utils";
import { queueImageForLoading } from "@/services/ImageLoader";
export default defineComponent({ export default defineComponent({
name: "VideoScroll", name: "VideoScroll",
@ -87,35 +88,46 @@ export default defineComponent({
: -1; : -1;
}, },
}, },
methods: {
handleImageLoad(element: HTMLImageElement) {
// Setup image display when loaded
element.classList.add("displayed");
element.classList.add("loaded");
// Adjust dimensions based on scroll direction
if (this.isHorizontal) {
element.style.height = "auto";
} else {
element.style.width = "auto";
}
}
},
mounted() { mounted() {
const observer = new IntersectionObserver((entries, _) => { const observer = new IntersectionObserver((entries, _) => {
entries.forEach((entry) => { entries.forEach((entry) => {
const element = entry.target as HTMLImageElement; const element = entry.target as HTMLImageElement;
if (entry.isIntersecting) { if (entry.isIntersecting) {
element.classList.add("visible"); element.classList.add("visible");
if (!element.src) { if (!element.src && element.dataset.src) {
console.debug( // Queue the image for loading through the global service
`[VIDEOSCROLL] Intersected, loading ${element.dataset.src}` const self = this;
); queueImageForLoading(element, function() {
element.src = element.dataset.src!; self.handleImageLoad(element);
});
// Add a fallback to show the image after a timeout even if not fully loaded
setTimeout(() => { setTimeout(() => {
element.classList.add("displayed"); if (!element.classList.contains("loaded")) {
}, 3000); element.classList.add("displayed");
element.onload = () => {
element.classList.add("displayed");
element.classList.add("loaded");
if (this.isHorizontal) {
element.style.height = "auto";
} else {
element.style.width = "auto";
} }
}; }, 3000);
} }
} else { } else {
element.classList.remove("visible"); element.classList.remove("visible");
} }
}); });
}); });
if (this.$refs.root) { if (this.$refs.root) {
Array.from((this.$refs.root as Element).children).forEach((el) => { Array.from((this.$refs.root as Element).children).forEach((el) => {
observer.observe(el); observer.observe(el);

View file

@ -0,0 +1,80 @@
/**
* Global image loading queue service to prevent hitting browser connection limits
*/
// Configuration
const MAX_CONCURRENT_LOADS = 5;
// State
let activeLoads = 0;
const imageQueue: Array<{
element: HTMLImageElement;
onComplete: () => void;
}> = [];
/**
* Queue an image for loading, respecting the global concurrent loading limit
*/
export function queueImageForLoading(
element: HTMLImageElement,
onComplete?: () => void
) {
if (!element.dataset.src) {
console.warn("[ImageLoader] Element has no data-src attribute");
return;
}
// Add to queue
imageQueue.push({
element,
onComplete: onComplete || (() => {}),
});
// Try to process queue
processQueue();
}
/**
* Process the next items in the queue if we have capacity
*/
function processQueue() {
// Load more images if we have capacity and images in the queue
while (activeLoads < MAX_CONCURRENT_LOADS && imageQueue.length > 0) {
const next = imageQueue.shift();
if (next) {
loadImage(next.element, next.onComplete);
}
}
}
/**
* Internal function to handle the actual image loading
*/
function loadImage(element: HTMLImageElement, onComplete: () => void) {
// Increment active loads counter
activeLoads++;
const src = element.dataset.src;
console.debug(`[ImageLoader] Loading ${src}`);
// Start loading the image
element.src = src!;
// Handle load completion
const handleCompletion = () => {
activeLoads--;
onComplete();
processQueue();
};
// Set handlers
element.onload = () => {
console.debug(`[ImageLoader] Loaded ${src}`);
handleCompletion();
};
element.onerror = () => {
console.error(`[ImageLoader] Failed to load ${src}`);
handleCompletion();
};
}