diff --git a/src/misc.ts b/src/misc.ts index 6bdb52b..0ec4458 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -676,6 +676,28 @@ export const isSafari = () => { return result; }; +let isWebKitCache: boolean | null = null; +export const isWebKit = () => { + if (isWebKitCache !== null) return isWebKitCache; + if (typeof navigator === 'undefined') return (isWebKitCache = false); + + const ua = navigator.userAgent || ''; + const maxTouchPoints = navigator.maxTouchPoints || 0; + + // iOS/iPadOS detection: + // - All iOS/iPadOS browsers use WebKit (WKWebView) + // - iPadOS 13+ can report "Macintosh" in UA; detect via touch points + const isIOSLike + = /iPhone|iPad|iPod/i.test(ua) || (/Macintosh/i.test(ua) && maxTouchPoints > 1); + + // On iOS/iPadOS: always WebKit (even if UA says CriOS/Edg/OPR/etc.) + if (isIOSLike) return (isWebKitCache = true); + + // Off iOS: only Safari is WebKit on mainstream desktops + const result = isSafari(); + return (isWebKitCache = result); +}; + let isFirefoxCache: boolean | null = null; export const isFirefox = () => { if (isFirefoxCache !== null) { diff --git a/src/source.ts b/src/source.ts index adafa2e..91fe2b2 100644 --- a/src/source.ts +++ b/src/source.ts @@ -12,6 +12,7 @@ import { binarySearchLessOrEqual, closedIntervalsOverlap, isNumber, + isWebKit, MaybePromise, mergeRequestInit, promiseWithResolvers, @@ -209,7 +210,13 @@ export class BlobSource extends Source { private async _runWorker(worker: ReadWorker) { let reader = this._readers.get(worker); if (reader === undefined) { - if ('stream' in this._blob) { + // WebKit has critical bugs with blob.stream(): + // - WebKitBlobResource error 1 when streaming large files + // - Memory buildup and reload loops on iOS (network process crashes) + // - ReadableStream stalls under backpressure (especially video) + // Affects Safari and all iOS browsers (Chrome, Firefox, etc.). + // Use arrayBuffer() fallback for WebKit browsers. + if ('stream' in this._blob && !isWebKit()) { // Get a reader of the blob starting at the required offset, and then keep it around const slice = this._blob.slice(worker.currentPos); reader = slice.stream().getReader();