blastoise-archive/public/audioCache.js

206 lines
7.3 KiB
JavaScript

// MusicRoom - Audio Cache module
// Track caching, downloading, and prefetching
(function() {
const M = window.MusicRoom;
// Get or create cache for a track
M.getTrackCache = function(trackId) {
if (!trackId) return new Set();
if (!M.trackCaches.has(trackId)) {
M.trackCaches.set(trackId, new Set());
}
return M.trackCaches.get(trackId);
};
// Get track URL - prefers cached blob, falls back to API
M.getTrackUrl = function(trackId) {
return M.trackBlobs.get(trackId) || "/api/tracks/" + encodeURIComponent(trackId);
};
// Load a track blob from storage or fetch from server
M.loadTrackBlob = async function(trackId) {
// Check if already in memory
if (M.trackBlobs.has(trackId)) {
return M.trackBlobs.get(trackId);
}
// Check persistent storage
const cached = await TrackStorage.get(trackId);
if (cached) {
const blobUrl = URL.createObjectURL(cached.blob);
M.trackBlobs.set(trackId, blobUrl);
// Mark all segments as cached
const trackCache = M.getTrackCache(trackId);
for (let i = 0; i < M.SEGMENTS; i++) trackCache.add(i);
M.bulkDownloadStarted.set(trackId, true);
// Update cache status indicator
if (!M.cachedTracks.has(trackId)) {
M.cachedTracks.add(trackId);
M.renderQueue();
M.renderLibrary();
}
return blobUrl;
}
return null;
};
// Check if track has all segments and trigger full cache if so
M.checkAndCacheComplete = function(trackId) {
if (!trackId || M.cachedTracks.has(trackId)) return;
const trackCache = M.trackCaches.get(trackId);
if (trackCache && trackCache.size >= M.SEGMENTS) {
console.log("[Cache] Track has all segments, triggering full cache:", trackId.slice(0, 16) + "...");
M.downloadAndCacheTrack(trackId);
}
};
// Download and cache a full track
M.downloadAndCacheTrack = async function(trackId) {
if (M.bulkDownloadStarted.get(trackId)) return M.trackBlobs.get(trackId);
M.bulkDownloadStarted.set(trackId, true);
try {
const startTime = performance.now();
const res = await fetch("/api/tracks/" + encodeURIComponent(trackId));
const data = await res.arrayBuffer();
const elapsed = (performance.now() - startTime) / 1000;
// Mark all segments as cached
const trackCache = M.getTrackCache(trackId);
for (let i = 0; i < M.SEGMENTS; i++) trackCache.add(i);
// Create blob and URL
const contentType = res.headers.get("Content-Type") || "audio/mpeg";
const blob = new Blob([data], { type: contentType });
const blobUrl = URL.createObjectURL(blob);
M.trackBlobs.set(trackId, blobUrl);
// Persist to storage
await TrackStorage.set(trackId, blob, contentType);
// Update cache status and re-render lists
console.log("[Cache] Track cached:", trackId.slice(0, 16) + "...", "| size:", (data.byteLength / 1024 / 1024).toFixed(2) + "MB");
M.cachedTracks.add(trackId);
M.renderQueue();
M.renderLibrary();
// Update download speed
if (elapsed > 0 && data.byteLength > 0) {
M.recentDownloads.push(data.byteLength / elapsed);
if (M.recentDownloads.length > 5) M.recentDownloads.shift();
M.downloadSpeed = M.recentDownloads.reduce((a, b) => a + b, 0) / M.recentDownloads.length;
}
return blobUrl;
} catch (e) {
M.bulkDownloadStarted.set(trackId, false);
return null;
}
};
// Fetch a single segment with range request
async function fetchSegment(i, segStart, segEnd) {
const trackId = M.currentTrackId;
const trackCache = M.getTrackCache(trackId);
if (M.loadingSegments.has(i) || trackCache.has(i)) return;
M.loadingSegments.add(i);
try {
const byteStart = Math.floor(segStart * M.audioBytesPerSecond);
const byteEnd = Math.floor(segEnd * M.audioBytesPerSecond);
const startTime = performance.now();
const res = await fetch("/api/tracks/" + encodeURIComponent(trackId), {
headers: { "Range": `bytes=${byteStart}-${byteEnd}` }
});
const data = await res.arrayBuffer();
const elapsed = (performance.now() - startTime) / 1000;
// Mark segment as cached
trackCache.add(i);
// Check if all segments are now cached - if so, trigger full cache
if (trackCache.size >= M.SEGMENTS && !M.cachedTracks.has(trackId)) {
console.log("[Cache] All segments complete for:", trackId.slice(0, 16) + "...", "- triggering full cache");
// Download full track to persist to storage
M.downloadAndCacheTrack(trackId);
}
// Update audio bitrate estimate
const bytesReceived = data.byteLength;
const durationCovered = segEnd - segStart;
if (bytesReceived > 0 && durationCovered > 0) {
M.audioBytesPerSecond = Math.round(bytesReceived / durationCovered);
}
// Update download speed (rolling average of last 5 downloads)
if (elapsed > 0 && bytesReceived > 0) {
M.recentDownloads.push(bytesReceived / elapsed);
if (M.recentDownloads.length > 5) M.recentDownloads.shift();
M.downloadSpeed = M.recentDownloads.reduce((a, b) => a + b, 0) / M.recentDownloads.length;
}
} catch (e) {}
M.loadingSegments.delete(i);
}
// Background bulk download - runs independently
async function startBulkDownload() {
const trackId = M.currentTrackId;
if (!trackId || M.bulkDownloadStarted.get(trackId)) return;
const blobUrl = await M.downloadAndCacheTrack(trackId);
// Switch to blob URL if still on this track
if (blobUrl && M.currentTrackId === trackId && M.audio.src && !M.audio.src.startsWith("blob:")) {
const currentTime = M.audio.currentTime;
const wasPlaying = !M.audio.paused;
M.audio.src = blobUrl;
M.audio.currentTime = currentTime;
if (wasPlaying) M.audio.play().catch(() => {});
}
}
// Prefetch missing segments
let prefetching = false;
M.prefetchSegments = async function() {
if (prefetching || !M.currentTrackId || !M.audio.src || M.serverTrackDuration <= 0) return;
prefetching = true;
const segmentDur = M.serverTrackDuration / M.SEGMENTS;
const missingSegments = [];
const trackCache = M.getTrackCache(M.currentTrackId);
// Find all missing segments (not in audio buffer AND not in our cache)
for (let i = 0; i < M.SEGMENTS; i++) {
if (trackCache.has(i) || M.loadingSegments.has(i)) continue;
const segStart = i * segmentDur;
const segEnd = (i + 1) * segmentDur;
let available = false;
for (let j = 0; j < M.audio.buffered.length; j++) {
if (M.audio.buffered.start(j) <= segStart && M.audio.buffered.end(j) >= segEnd) {
available = true;
break;
}
}
if (!available) {
missingSegments.push({ i, segStart, segEnd });
}
}
if (missingSegments.length > 0) {
// Fast connection: also start bulk download in background
if (M.downloadSpeed >= M.FAST_THRESHOLD && !M.bulkDownloadStarted.get(M.currentTrackId)) {
startBulkDownload(); // Fire and forget
}
// Always fetch segments one at a time for seek support
const s = missingSegments[0];
await fetchSegment(s.i, s.segStart, s.segEnd);
}
prefetching = false;
};
})();