feat: implement image queue service and audio preloading to prevent connection limits
This commit is contained in:
		
							parent
							
								
									5f410715ca
								
							
						
					
					
						commit
						0afb320728
					
				
					 3 changed files with 137 additions and 16 deletions
				
			
		| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <audio ref="audio" :src="definition.src" loop preload="auto" />
 | 
			
		||||
  <audio ref="audio" :src="audioSrc" loop preload="auto" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
| 
						 | 
				
			
			@ -20,9 +20,37 @@ export default defineComponent({
 | 
			
		|||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    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(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_VOLUME_MULTIPLIER = 0.33;
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +89,7 @@ export default defineComponent({
 | 
			
		|||
 | 
			
		||||
    return {
 | 
			
		||||
      audio,
 | 
			
		||||
      audioSrc,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
import { defineComponent, PropType } from "vue";
 | 
			
		||||
import { rotate } from "@/utils";
 | 
			
		||||
import { queueImageForLoading } from "@/services/ImageLoader";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "VideoScroll",
 | 
			
		||||
| 
						 | 
				
			
			@ -87,35 +88,46 @@ export default defineComponent({
 | 
			
		|||
        : -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() {
 | 
			
		||||
    const observer = new IntersectionObserver((entries, _) => {
 | 
			
		||||
      entries.forEach((entry) => {
 | 
			
		||||
        const element = entry.target as HTMLImageElement;
 | 
			
		||||
        if (entry.isIntersecting) {
 | 
			
		||||
          element.classList.add("visible");
 | 
			
		||||
          if (!element.src) {
 | 
			
		||||
            console.debug(
 | 
			
		||||
              `[VIDEOSCROLL] Intersected, loading ${element.dataset.src}`
 | 
			
		||||
            );
 | 
			
		||||
            element.src = element.dataset.src!;
 | 
			
		||||
          if (!element.src && element.dataset.src) {
 | 
			
		||||
            // Queue the image for loading through the global service
 | 
			
		||||
            const self = this;
 | 
			
		||||
            queueImageForLoading(element, function() {
 | 
			
		||||
              self.handleImageLoad(element);
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // Add a fallback to show the image after a timeout even if not fully loaded
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              element.classList.add("displayed");
 | 
			
		||||
            }, 3000);
 | 
			
		||||
            element.onload = () => {
 | 
			
		||||
              element.classList.add("displayed");
 | 
			
		||||
              element.classList.add("loaded");
 | 
			
		||||
              if (this.isHorizontal) {
 | 
			
		||||
                element.style.height = "auto";
 | 
			
		||||
              } else {
 | 
			
		||||
                element.style.width = "auto";
 | 
			
		||||
              if (!element.classList.contains("loaded")) {
 | 
			
		||||
                element.classList.add("displayed");
 | 
			
		||||
              }
 | 
			
		||||
            };
 | 
			
		||||
            }, 3000);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          element.classList.remove("visible");
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (this.$refs.root) {
 | 
			
		||||
      Array.from((this.$refs.root as Element).children).forEach((el) => {
 | 
			
		||||
        observer.observe(el);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										80
									
								
								src/services/ImageLoader.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/services/ImageLoader.ts
									
										
									
									
									
										Normal 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();
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue