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:
parent
485c4d5d98
commit
8d8130109d
@ -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).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user