@@ -315,121 +315,44 @@ export class BinaryMedia {
315315 element : HTMLElement ,
316316 streamRef : { stream : ( ) => Promise < ReadableStream < Uint8Array > > } | null ,
317317 mimeType : string ,
318- cacheKey : string ,
319318 totalBytes : number | null ,
320- fileName ?: string | null ,
321- attemptNativePicker = true ,
322- ) : Promise < MediaLoadResult > {
323- if ( ! element || ! cacheKey ) {
324- return { success : false , fromCache : false , objectUrl : null , error : 'Invalid parameters' } ;
325- }
326-
327- const nativePickerAvailable = attemptNativePicker && typeof ( window as unknown as { showSaveFilePicker ?: unknown } ) . showSaveFilePicker === 'function' ;
328-
329- // Try cache first
330- let cacheHitBlob : Blob | null = null ;
331- let _fromCache = false ;
332- try {
333- const cache = await this . getCache ( ) ;
334- if ( cache ) {
335- const cached = await cache . match ( encodeURIComponent ( cacheKey ) ) ;
336- if ( cached ) {
337- cacheHitBlob = await cached . blob ( ) ;
338- _fromCache = true ;
339- }
340- }
341- } catch ( err ) {
342- this . logger . log ( LogLevel . Debug , `Cache lookup failed (download): ${ err } ` ) ;
343- }
344-
345- // If we have a cache hit and native picker is available, stream blob to file directly
346- if ( cacheHitBlob && nativePickerAvailable ) {
347- try {
348- const handle = await ( window as unknown as { showSaveFilePicker : ( opts : any ) => Promise < any > } ) . showSaveFilePicker ( { suggestedName : fileName || cacheKey } ) ; // eslint-disable-line @typescript-eslint/no-explicit-any
349- const writer = await handle . createWritable ( ) ;
350- const stream = cacheHitBlob . stream ( ) ;
351- const result = await this . writeStreamToFile ( element , stream as ReadableStream < Uint8Array > , writer , totalBytes ) ;
352- if ( result === 'success' ) {
353- return { success : true , fromCache : true , objectUrl : null } ;
354- }
355- // aborted treated as failure
356- return { success : false , fromCache : true , objectUrl : null , error : 'Aborted' } ;
357- } catch ( pickerErr ) {
358- // User might have cancelled; fall back to anchor download if we still have blob
359- this . logger . log ( LogLevel . Debug , `Native picker path failed or cancelled: ${ pickerErr } ` ) ;
360- }
361- }
362-
363- if ( cacheHitBlob && ! nativePickerAvailable ) {
364- // Fallback anchor path using cached blob
365- const url = URL . createObjectURL ( cacheHitBlob ) ;
366- this . triggerDownload ( url , ( fileName || cacheKey ) ) ;
367- return { success : true , fromCache : true , objectUrl : url } ;
368- }
369-
370- if ( ! streamRef ) {
371- return { success : false , fromCache : false , objectUrl : null , error : 'No stream provided' } ;
319+ fileName : string ,
320+ ) : Promise < boolean > {
321+ if ( ! element || ! fileName || ! streamRef ) {
322+ return false ;
372323 }
373324
374- // Stream and optionally cache (dup logic from streamAndCreateUrl, without setting element attributes)
375325 this . loadingElements . add ( element ) ;
376326 const controller = new AbortController ( ) ;
377327 this . controllers . set ( element , controller ) ;
378328
379329 try {
380330 const readable = await streamRef . stream ( ) ;
381331
382- // If native picker available, we can stream directly to file, optionally tee for cache
383- if ( nativePickerAvailable ) {
332+ // Native picker direct-to- file streaming available
333+ if ( typeof ( window as any ) . showSaveFilePicker === 'function' ) { // eslint-disable-line @typescript-eslint/no-explicit-any
384334 try {
385- const handle = await ( window as unknown as { showSaveFilePicker : ( opts : any ) => Promise < any > } ) . showSaveFilePicker ( { suggestedName : fileName || cacheKey } ) ; // eslint-disable-line @typescript-eslint/no-explicit-any
386- const writer = await handle . createWritable ( ) ;
387-
388- let workingStream : ReadableStream < Uint8Array > = readable ;
389- let cacheStream : ReadableStream < Uint8Array > | null = null ;
390- if ( cacheKey ) {
391- const cache = await this . getCache ( ) ;
392- if ( cache ) {
393- const tees = readable . tee ( ) ;
394- workingStream = tees [ 0 ] ;
395- cacheStream = tees [ 1 ] ;
396- cache . put ( encodeURIComponent ( cacheKey ) , new Response ( cacheStream ) ) . catch ( err => {
397- this . logger . log ( LogLevel . Debug , `Failed to put cache entry (download/native): ${ err } ` ) ;
398- } ) ;
399- }
400- }
335+ const handle = await ( window as any ) . showSaveFilePicker ( { suggestedName : fileName } ) ; // eslint-disable-line @typescript-eslint/no-explicit-any
401336
402- const writeResult = await this . writeStreamToFile ( element , workingStream , writer , totalBytes , controller ) ;
337+ const writer = await handle . createWritable ( ) ;
338+ const writeResult = await this . writeStreamToFile ( element , readable , writer , totalBytes , controller ) ;
403339 if ( writeResult === 'success' ) {
404- return { success : true , fromCache : false , objectUrl : null } ;
340+ return true ;
405341 }
406342 if ( writeResult === 'aborted' ) {
407- return { success : false , fromCache : false , objectUrl : null , error : 'Aborted' } ;
343+ return false ;
408344 }
409345 } catch ( pickerErr ) {
410- this . logger . log ( LogLevel . Debug , `Native picker streaming path failed or cancelled after selection: ${ pickerErr } ` ) ;
411- // Fall through to in-memory blob fallback
412- }
413- }
414-
415- // In-memory path (existing logic)
416- let displayStream : ReadableStream < Uint8Array > = readable ;
417- if ( cacheKey ) {
418- const cache = await this . getCache ( ) ;
419- if ( cache ) {
420- const [ display , cacheStream ] = readable . tee ( ) ;
421- displayStream = display ;
422- cache . put ( encodeURIComponent ( cacheKey ) , new Response ( cacheStream ) ) . catch ( err => {
423- this . logger . log ( LogLevel . Debug , `Failed to put cache entry (download): ${ err } ` ) ;
424- } ) ;
346+ this . logger . log ( LogLevel . Debug , `Native picker streaming path failed or cancelled: ${ pickerErr } ` ) ;
425347 }
426348 }
427349
350+ // In-memory fallback: read all bytes then trigger anchor download
428351 const chunks : Uint8Array [ ] = [ ] ;
429352 let bytesRead = 0 ;
430- for await ( const chunk of this . iterateStream ( displayStream , controller . signal ) ) {
353+ for await ( const chunk of this . iterateStream ( readable , controller . signal ) ) {
431354 if ( controller . signal . aborted ) {
432- return { success : false , fromCache : false , objectUrl : null , error : 'Aborted' } ;
355+ return false ;
433356 }
434357 chunks . push ( chunk ) ;
435358 bytesRead += chunk . byteLength ;
@@ -440,17 +363,18 @@ export class BinaryMedia {
440363 }
441364
442365 if ( bytesRead === 0 ) {
443- return { success : false , fromCache : false , objectUrl : null , error : 'Empty stream' } ;
366+ return false ;
444367 }
445368
446369 const combined = this . combineChunks ( chunks ) ;
447370 const blob = new Blob ( [ combined ] , { type : mimeType } ) ;
448371 const url = URL . createObjectURL ( blob ) ;
449- this . triggerDownload ( url , fileName || cacheKey ) ;
450- return { success : true , fromCache : false , objectUrl : url } ;
372+ this . triggerDownload ( url , fileName ) ;
373+
374+ return true ;
451375 } catch ( error ) {
452376 this . logger . log ( LogLevel . Debug , `Error in downloadAsync: ${ error } ` ) ;
453- return { success : false , fromCache : false , objectUrl : null , error : String ( error ) } ;
377+ return false ;
454378 } finally {
455379 if ( this . controllers . get ( element ) === controller ) {
456380 this . controllers . delete ( element ) ;
@@ -531,9 +455,11 @@ export class BinaryMedia {
531455 a . style . display = 'none' ;
532456 document . body . appendChild ( a ) ;
533457 a . click ( ) ;
458+
534459 setTimeout ( ( ) => {
535460 try {
536- document . body . removeChild ( a ) ;
461+ a . remove ( ) ;
462+ URL . revokeObjectURL ( url ) ;
537463 } catch {
538464 // ignore
539465 }
@@ -543,18 +469,16 @@ export class BinaryMedia {
543469 }
544470 }
545471
546- // Helper to stream data directly to a FileSystemWritableFileStream with progress & abort handling
547472 private static async writeStreamToFile (
548473 element : HTMLElement ,
549474 stream : ReadableStream < Uint8Array > ,
550475 writer : any , // eslint-disable-line @typescript-eslint/no-explicit-any
551476 totalBytes : number | null ,
552477 controller ?: AbortController
553478 ) : Promise < 'success' | 'aborted' | 'error' > {
554- const reader = stream . getReader ( ) ;
555479 let written = 0 ;
556480 try {
557- for ( ; ; ) {
481+ for await ( const chunk of this . iterateStream ( stream , controller ?. signal ) ) {
558482 if ( controller ?. signal . aborted ) {
559483 try {
560484 await writer . abort ( ) ;
@@ -564,20 +488,44 @@ export class BinaryMedia {
564488 element . style . removeProperty ( '--blazor-media-progress' ) ;
565489 return 'aborted' ;
566490 }
567- const { done , value } = await reader . read ( ) ;
568- if ( done ) {
569- break ;
570- }
571- if ( value ) {
572- await writer . write ( value ) ;
573- written += value . byteLength ;
574- if ( totalBytes ) {
575- const progress = Math . min ( 1 , written / totalBytes ) ;
576- element . style . setProperty ( '--blazor-media-progress' , progress . toString ( ) ) ;
491+ try {
492+ await writer . write ( chunk ) ;
493+ } catch ( wErr ) {
494+ if ( controller ?. signal . aborted ) {
495+ try {
496+ await writer . abort ( ) ;
497+ } catch {
498+ /* ignore */
499+ }
500+ return 'aborted' ;
577501 }
502+ return 'error' ;
503+ }
504+ written += chunk . byteLength ;
505+ if ( totalBytes ) {
506+ const progress = Math . min ( 1 , written / totalBytes ) ;
507+ element . style . setProperty ( '--blazor-media-progress' , progress . toString ( ) ) ;
508+ }
509+ }
510+
511+ if ( controller ?. signal . aborted ) {
512+ try {
513+ await writer . abort ( ) ;
514+ } catch {
515+ /* ignore */
516+ }
517+ element . style . removeProperty ( '--blazor-media-progress' ) ;
518+ return 'aborted' ;
519+ }
520+
521+ try {
522+ await writer . close ( ) ;
523+ } catch ( closeErr ) {
524+ if ( controller ?. signal . aborted ) {
525+ return 'aborted' ;
578526 }
527+ return 'error' ;
579528 }
580- await writer . close ( ) ;
581529 return 'success' ;
582530 } catch ( e ) {
583531 try {
@@ -588,11 +536,6 @@ export class BinaryMedia {
588536 return controller ?. signal . aborted ? 'aborted' : 'error' ;
589537 } finally {
590538 element . style . removeProperty ( '--blazor-media-progress' ) ;
591- try {
592- reader . releaseLock ?.( ) ;
593- } catch {
594- /* ignore */
595- }
596539 }
597540 }
598541}
0 commit comments