fix: video/audio streaming instead of blob download

Videos and audio now stream directly via URL with auth token query
param instead of downloading entire file into a JS blob. Fixes
playback of large videos (170MB+ was timing out). Images still use
blob URLs. streamUrl() added to filebrowser client and cloud store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-12 00:45:42 -04:00
parent 485c4d5d98
commit 8d8130109d
4 changed files with 34 additions and 5 deletions

View File

@ -122,6 +122,7 @@ class FileBrowserClient {
/** /**
* Fetch a file as a blob URL using header-based auth (no token in URL). * Fetch a file as a blob URL using header-based auth (no token in URL).
* Use this for img/video/audio src attributes and download links. * Use this for img/video/audio src attributes and download links.
* For large files (video/audio), prefer streamUrl() instead.
*/ */
async fetchBlobUrl(path: string): Promise<string> { async fetchBlobUrl(path: string): Promise<string> {
await this.ensureAuth() await this.ensureAuth()
@ -134,6 +135,18 @@ class FileBrowserClient {
return URL.createObjectURL(blob) return URL.createObjectURL(blob)
} }
/**
* Get a direct streaming URL with auth token in query string.
* Use for video/audio <src> where browser needs to stream (range requests).
* The token is a short-lived JWT so exposure in URL is acceptable.
*/
async streamUrl(path: string): Promise<string> {
await this.ensureAuth()
const token = this.getAuthCookie()
const safePath = sanitizePath(path)
return `${this.baseUrl}/api/raw${safePath}?auth=${token}`
}
/** /**
* Trigger a file download using header-based auth (no token in URL). * Trigger a file download using header-based auth (no token in URL).
*/ */

View File

@ -117,6 +117,7 @@ const props = defineProps<{
startIndex: number startIndex: number
show: boolean show: boolean
fetchBlobUrl: (path: string) => Promise<string> fetchBlobUrl: (path: string) => Promise<string>
streamUrl?: (path: string) => Promise<string>
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -167,9 +168,18 @@ async function loadMedia(item: FileBrowserItem) {
if (cached) { if (cached) {
currentUrl.value = cached currentUrl.value = cached
} else { } else {
const url = await props.fetchBlobUrl(item.path) // Use streaming URL for video/audio (avoids downloading entire file into blob)
urlCache.set(item.path, url) // Use blob URL for images (needed for rendering)
currentUrl.value = url const isStreamable = isVideoFile(item) || isAudioFile(item)
if (isStreamable && props.streamUrl) {
const url = await props.streamUrl(item.path)
urlCache.set(item.path, url)
currentUrl.value = url
} else {
const url = await props.fetchBlobUrl(item.path)
urlCache.set(item.path, url)
currentUrl.value = url
}
} }
} catch { } catch {
mediaError.value = true mediaError.value = true

View File

@ -99,6 +99,10 @@ export const useCloudStore = defineStore('cloud', () => {
return fileBrowserClient.fetchBlobUrl(path) return fileBrowserClient.fetchBlobUrl(path)
} }
async function streamUrl(path: string): Promise<string> {
return fileBrowserClient.streamUrl(path)
}
async function downloadFile(path: string): Promise<void> { async function downloadFile(path: string): Promise<void> {
return fileBrowserClient.downloadFile(path) return fileBrowserClient.downloadFile(path)
} }
@ -125,6 +129,7 @@ export const useCloudStore = defineStore('cloud', () => {
deleteItem, deleteItem,
downloadUrl, downloadUrl,
fetchBlobUrl, fetchBlobUrl,
streamUrl,
downloadFile, downloadFile,
reset, reset,
} }

View File

@ -161,6 +161,7 @@
:start-index="lightboxIndex" :start-index="lightboxIndex"
:show="lightboxIndex !== null" :show="lightboxIndex !== null"
:fetch-blob-url="cloudStore.fetchBlobUrl" :fetch-blob-url="cloudStore.fetchBlobUrl"
:stream-url="cloudStore.streamUrl"
@close="lightboxIndex = null" @close="lightboxIndex = null"
/> />
</div> </div>
@ -358,8 +359,8 @@ async function handleDelete(path: string) {
await cloudStore.deleteItem(path) await cloudStore.deleteItem(path)
} }
function handlePlay(path: string, name: string) { async function handlePlay(path: string, name: string) {
const url = cloudStore.downloadUrl(path) const url = await cloudStore.streamUrl(path)
audioPlayer.play(url, name) audioPlayer.play(url, name)
} }