Skip to content

Commit 7c4db03

Browse files
committed
Fixed tests
1 parent 4c94d01 commit 7c4db03

File tree

6 files changed

+184
-336
lines changed

6 files changed

+184
-336
lines changed

src/Components/Web.JS/src/Rendering/BinaryMedia.ts

Lines changed: 153 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)