@@ -197,27 +197,6 @@ export class BinaryMedia {
197197 }
198198 }
199199
200- private static setUrl ( element : HTMLElement , url : string , cacheKey : string , targetAttr : 'src' | 'href' ) : void {
201- const tracked = this . tracked . get ( element ) ;
202- if ( tracked ) {
203- try {
204- URL . revokeObjectURL ( tracked . url ) ;
205- } catch {
206- // ignore
207- }
208- }
209-
210- this . tracked . set ( element , { url, cacheKey, attr : targetAttr } ) ;
211-
212- if ( targetAttr === 'src' ) {
213- ( element as HTMLImageElement | HTMLVideoElement ) . src = url ;
214- } else {
215- ( element as HTMLAnchorElement ) . href = url ;
216- }
217-
218- this . setupEventHandlers ( element , cacheKey ) ;
219- }
220-
221200 private static async streamAndCreateUrl (
222201 element : HTMLElement ,
223202 streamRef : { stream : ( ) => Promise < ReadableStream < Uint8Array > > } ,
@@ -246,25 +225,9 @@ export class BinaryMedia {
246225 }
247226 }
248227
249- const chunks : Uint8Array [ ] = [ ] ;
250- let bytesRead = 0 ;
251- let aborted = false ;
252228 let resultUrl : string | null = null ;
253-
254229 try {
255- for await ( const chunk of this . iterateStream ( displayStream , controller . signal ) ) {
256- if ( controller . signal . aborted ) { // Stream aborted due to a new setImageAsync call with a key change
257- aborted = true ;
258- break ;
259- }
260- chunks . push ( chunk ) ;
261- bytesRead += chunk . byteLength ;
262-
263- if ( totalBytes ) {
264- const progress = Math . min ( 1 , bytesRead / totalBytes ) ;
265- element . style . setProperty ( '--blazor-media-progress' , progress . toString ( ) ) ;
266- }
267- }
230+ const { aborted, chunks, bytesRead } = await this . readAllChunks ( element , displayStream , controller , totalBytes ) ;
268231
269232 if ( ! aborted ) {
270233 if ( bytesRead === 0 ) {
@@ -293,6 +256,28 @@ export class BinaryMedia {
293256 return resultUrl ;
294257 }
295258
259+ private static async readAllChunks (
260+ element : HTMLElement ,
261+ stream : ReadableStream < Uint8Array > ,
262+ controller : AbortController ,
263+ totalBytes : number | null
264+ ) : Promise < { aborted : boolean ; chunks : Uint8Array [ ] ; bytesRead : number } > {
265+ const chunks : Uint8Array [ ] = [ ] ;
266+ let bytesRead = 0 ;
267+ for await ( const chunk of this . iterateStream ( stream , controller . signal ) ) {
268+ if ( controller . signal . aborted ) {
269+ return { aborted : true , chunks, bytesRead } ;
270+ }
271+ chunks . push ( chunk ) ;
272+ bytesRead += chunk . byteLength ;
273+ if ( totalBytes ) {
274+ const progress = Math . min ( 1 , bytesRead / totalBytes ) ;
275+ element . style . setProperty ( '--blazor-media-progress' , progress . toString ( ) ) ;
276+ }
277+ }
278+ return { aborted : controller . signal . aborted , chunks, bytesRead } ;
279+ }
280+
296281 private static combineChunks ( chunks : Uint8Array [ ] ) : Uint8Array {
297282 if ( chunks . length === 1 ) {
298283 return chunks [ 0 ] ;
@@ -308,29 +293,29 @@ export class BinaryMedia {
308293 return combined ;
309294 }
310295
311- private static setupEventHandlers (
312- element : HTMLElement ,
313- cacheKey : string | null = null
314- ) : void {
315- const onLoad = ( _e : Event ) => {
316- if ( ! cacheKey || BinaryMedia . activeCacheKey . get ( element ) === cacheKey ) {
317- BinaryMedia . loadingElements . delete ( element ) ;
318- element . style . removeProperty ( '--blazor-media-progress' ) ;
296+ private static setUrl ( element : HTMLElement , url : string , cacheKey : string , targetAttr : 'src' | 'href' ) : void {
297+ const tracked = this . tracked . get ( element ) ;
298+ if ( tracked ) {
299+ try {
300+ URL . revokeObjectURL ( tracked . url ) ;
301+ } catch {
302+ // ignore
319303 }
320- } ;
304+ }
321305
322- const onError = ( _e : Event ) => {
323- if ( ! cacheKey || BinaryMedia . activeCacheKey . get ( element ) === cacheKey ) {
324- BinaryMedia . loadingElements . delete ( element ) ;
325- element . style . removeProperty ( '--blazor-media-progress' ) ;
326- element . setAttribute ( 'data-state' , 'error' ) ;
327- }
328- } ;
306+ this . tracked . set ( element , { url, cacheKey, attr : targetAttr } ) ;
329307
330- element . addEventListener ( 'load' , onLoad , { once : true } ) ;
331- element . addEventListener ( 'error' , onError , { once : true } ) ;
308+ if ( targetAttr === 'src' ) {
309+ ( element as HTMLImageElement | HTMLVideoElement ) . src = url ;
310+ } else {
311+ ( element as HTMLAnchorElement ) . href = url ;
312+ }
313+
314+ this . setupEventHandlers ( element , cacheKey ) ;
332315 }
333316
317+ // Streams binary content to a user-selected file when possible,
318+ // otherwise falls back to buffering in memory and triggering a blob download via an anchor.
334319 public static async downloadAsync (
335320 element : HTMLElement ,
336321 streamRef : { stream : ( ) => Promise < ReadableStream < Uint8Array > > } | null ,
@@ -368,21 +353,11 @@ export class BinaryMedia {
368353 }
369354
370355 // In-memory fallback: read all bytes then trigger anchor download
371- const chunks : Uint8Array [ ] = [ ] ;
372- let bytesRead = 0 ;
373- for await ( const chunk of this . iterateStream ( readable , controller . signal ) ) {
374- if ( controller . signal . aborted ) {
375- return false ;
376- }
377- chunks . push ( chunk ) ;
378- bytesRead += chunk . byteLength ;
379- if ( totalBytes ) {
380- const progress = Math . min ( 1 , bytesRead / totalBytes ) ;
381- element . style . setProperty ( '--blazor-media-progress' , progress . toString ( ) ) ;
382- }
356+ const readResult = await this . readAllChunks ( element , readable , controller , totalBytes ) ;
357+ if ( readResult . aborted ) {
358+ return false ;
383359 }
384-
385- const combined = this . combineChunks ( chunks ) ;
360+ const combined = this . combineChunks ( readResult . chunks ) ;
386361 const blob = new Blob ( [ combined ] , { type : mimeType } ) ;
387362 const url = URL . createObjectURL ( blob ) ;
388363 this . triggerDownload ( url , fileName ) ;
@@ -400,91 +375,6 @@ export class BinaryMedia {
400375 }
401376 }
402377
403- private static async getCache ( ) : Promise < Cache | null > {
404- if ( ! ( 'caches' in window ) ) {
405- this . logger . log ( LogLevel . Warning , 'Cache API not supported in this browser' ) ;
406- return null ;
407- }
408-
409- if ( ! this . cachePromise ) {
410- this . cachePromise = ( async ( ) => {
411- try {
412- return await caches . open ( this . CACHE_NAME ) ;
413- } catch ( error ) {
414- this . logger . log ( LogLevel . Debug , `Failed to open cache: ${ error } ` ) ;
415- return null ;
416- }
417- } ) ( ) ;
418- }
419-
420- const cache = await this . cachePromise ;
421- // If opening failed previously, allow retry next time
422- if ( ! cache ) {
423- this . cachePromise = undefined ;
424- }
425- return cache ;
426- }
427-
428- private static async * iterateStream ( stream : ReadableStream < Uint8Array > , signal ?: AbortSignal ) : AsyncGenerator < Uint8Array , void , unknown > {
429- const reader = stream . getReader ( ) ;
430- let finished = false ;
431- try {
432- while ( true ) {
433- if ( signal ?. aborted ) {
434- try {
435- await reader . cancel ( ) ;
436- } catch {
437- // ignore
438- }
439- return ;
440- }
441- const { done, value } = await reader . read ( ) ;
442- if ( done ) {
443- finished = true ;
444- return ;
445- }
446- if ( value ) {
447- yield value ;
448- }
449- }
450- } finally {
451- if ( ! finished ) {
452- try {
453- await reader . cancel ( ) ;
454- } catch {
455- // ignore
456- }
457- }
458- try {
459- reader . releaseLock ?.( ) ;
460- } catch {
461- // ignore
462- }
463- }
464- }
465-
466- private static triggerDownload ( url : string , fileName : string ) : void {
467- try {
468- const a = document . createElement ( 'a' ) ;
469- a . href = url ;
470- a . download = fileName ;
471- a . style . display = 'none' ;
472- document . body . appendChild ( a ) ;
473- a . click ( ) ;
474-
475- setTimeout ( ( ) => {
476- try {
477- a . remove ( ) ;
478- URL . revokeObjectURL ( url ) ;
479- } catch {
480- // ignore
481- }
482- } , 0 ) ;
483- } catch {
484- // ignore
485- }
486- }
487-
488378 private static async writeStreamToFile (
489379 element : HTMLElement ,
490380 stream : ReadableStream < Uint8Array > ,
@@ -554,4 +444,112 @@ export class BinaryMedia {
554444 element . style . removeProperty ( '--blazor-media-progress' ) ;
555445 }
556446 }
447+
448+ private static triggerDownload ( url : string , fileName : string ) : void {
449+ try {
450+ const a = document . createElement ( 'a' ) ;
451+ a . href = url ;
452+ a . download = fileName ;
453+ a . style . display = 'none' ;
454+ document . body . appendChild ( a ) ;
455+ a . click ( ) ;
456+
457+ setTimeout ( ( ) => {
458+ try {
459+ a . remove ( ) ;
460+ URL . revokeObjectURL ( url ) ;
461+ } catch {
462+ // ignore
463+ }
464+ } , 0 ) ;
465+ } catch {
466+ // ignore
467+ }
468+ }
469+
470+ private static async getCache ( ) : Promise < Cache | null > {
471+ if ( ! ( 'caches' in window ) ) {
472+ this . logger . log ( LogLevel . Warning , 'Cache API not supported in this browser' ) ;
473+ return null ;
474+ }
475+
476+ if ( ! this . cachePromise ) {
477+ this . cachePromise = ( async ( ) => {
478+ try {
479+ return await caches . open ( this . CACHE_NAME ) ;
480+ } catch ( error ) {
481+ this . logger . log ( LogLevel . Debug , `Failed to open cache: ${ error } ` ) ;
482+ return null ;
483+ }
484+ } ) ( ) ;
485+ }
486+
487+ const cache = await this . cachePromise ;
488+ // If opening failed previously, allow retry next time
489+ if ( ! cache ) {
490+ this . cachePromise = undefined ;
491+ }
492+ return cache ;
493+ }
494+
495+ private static setupEventHandlers (
496+ element : HTMLElement ,
497+ cacheKey : string | null = null
498+ ) : void {
499+ const onLoad = ( _e : Event ) => {
500+ if ( ! cacheKey || BinaryMedia . activeCacheKey . get ( element ) === cacheKey ) {
501+ BinaryMedia . loadingElements . delete ( element ) ;
502+ element . style . removeProperty ( '--blazor-media-progress' ) ;
503+ }
504+ } ;
505+
506+ const onError = ( _e : Event ) => {
507+ if ( ! cacheKey || BinaryMedia . activeCacheKey . get ( element ) === cacheKey ) {
508+ BinaryMedia . loadingElements . delete ( element ) ;
509+ element . style . removeProperty ( '--blazor-media-progress' ) ;
510+ element . setAttribute ( 'data-state' , 'error' ) ;
511+ }
512+ } ;
513+
514+ element . addEventListener ( 'load' , onLoad , { once : true } ) ;
515+ element . addEventListener ( 'error' , onError , { once : true } ) ;
516+ }
517+
518+ private static async * iterateStream ( stream : ReadableStream < Uint8Array > , signal ?: AbortSignal ) : AsyncGenerator < Uint8Array , void , unknown > {
519+ const reader = stream . getReader ( ) ;
520+ let finished = false ;
521+ try {
522+ while ( true ) {
523+ if ( signal ?. aborted ) {
524+ try {
525+ await reader . cancel ( ) ;
526+ } catch {
527+ // ignore
528+ }
529+ return ;
530+ }
531+ const { done, value } = await reader . read ( ) ;
532+ if ( done ) {
533+ finished = true ;
534+ return ;
535+ }
536+ if ( value ) {
537+ yield value ;
538+ }
539+ }
540+ } finally {
541+ if ( ! finished ) {
542+ try {
543+ await reader . cancel ( ) ;
544+ } catch {
545+ // ignore
546+ }
547+ }
548+ try {
549+ reader . releaseLock ?.( ) ;
550+ } catch {
551+ // ignore
552+ }
553+ }
554+ }
557555}
0 commit comments