Skip to content

Conversation

rolandVi
Copy link
Contributor

@rolandVi rolandVi commented Sep 4, 2025

Media Component Suite

Summary

This PR introduces a suite of media components for efficiently handling non-HTTP sources (byte[]s or streams) with streaming through JS interop and browser-side caching. The suite includes Image, Video, and FileDownload which built on a shared foundation.

Features

  • Unified source model
    • MediaSource from byte[] or Stream with MimeType and CacheKey properties
    • Single-use design: one MediaSource instance corresponds to exactly one media operation
    • Properties: MimeType, CacheKey, Length
  • Automatic caching
    • Caching backed by the browser Cache Storage API, keyed by CacheKey
    • Shared cache across Image and Video components for efficient reuse
  • Stream-based transfer
    • Uses DotNetStreamReference for efficient streaming from .NET to JS
    • Supports progress tracking via CSS custom property --blazor-media-progress
    • Robust cancellation/race handling: only the latest request for an element is finalized
  • Minimal markup and CSS-driven UX
    • Components render native HTML elements with distinctive markers:
      • <img data-blazor-image>
      • <video data-blazor-video>
      • <a data-blazor-file-download>
    • data-state="loading" or "error" enables styling via CSS
  • Lifecycle and memory safety
    • Blob URL creation/revocation handled in JS
    • Automatic URL revocation when components are removed from DOM
    • Cancellation of in-progress operations when sources change

API surface

Base

  • Abstract MediaComponentBase class for shared functionality

Image Component

  • Parameters
    • Source: MediaSource (required)
    • AdditionalAttributes: forwarded to the underlying <img> (e.g., alt, class, style)
  • Render output
    • <img data-blazor-image data-state="loading|error" ... />

Video Component

  • Parameters
    • Source: MediaSource (required)
    • AdditionalAttributes: forwarded to the underlying <video> (e.g., controls, autoplay, loop)
  • Render output
    • <video data-blazor-video data-state="loading|error" ... ></video>

FileDownload Component

  • Parameters
    • Source: MediaSource (required)
    • FileName: string (required) - Name for the downloaded file
    • Text: string (optional) - Link text, defaults to "Download"
    • AdditionalAttributes: forwarded to the underlying <a> (e.g., class, style)
  • Render output
    • <a data-blazor-file-download data-state="loading|error" ... >Download</a>
  • Doesn't use caching since users usually download a file only once

Implementation (src\Components)

  • Web\src\Media\MediaComponentBase.cs
  • Web\src\Media\MediaSource.cs
  • Web\src\Media\Image.cs
  • Web\src\Media\Video.cs
  • Web\src\Media\FileDownload.cs
  • Web.JS\src\Rendering\BinaryMedia.ts
  • Tests
    • Unit tests - Web\test\Media
    • E2E test components - test\testassets\BasicTestApp\MediaTest
    • E2E tests
      • test\E2ETest\Tests\ImageTest.cs
      • test\E2ETest\Tests\VideoTest.cs
      • test\E2ETest\Tests\FileDownloadTest.cs

Performance considerations

  • Stream-based transfer avoids large memory allocations
  • Shared caching system prevents duplicate data storage
  • Blob URLs are revoked when components are removed from the DOM
  • Cache-first load avoids redundant streaming for repeated content
  • Direct-to-disk downloading when available

Examples

Image Component

@using Microsoft.AspNetCore.Components.Web.Media

<Image Source="new MediaSource(photoBytes, "image/jpeg", "photo-42")"
       alt="Product photo"
       style="max-width: 100%; height: auto;" />

Video Component

@using Microsoft.AspNetCore.Components.Web.Media

<Video Source="new MediaSource(videoBytes, "video/mp4", "product-demo")"
       controls
       style="max-width: 100%;" />

FileDownload Component

@using Microsoft.AspNetCore.Components.Web.Media

<FileDownload Source="new MediaSource(pdfData, "application/pdf", "report")"
              FileName="annual-report-2025.pdf"
              Text="Download Report" />

Related to -> #63562

rolandVi and others added 30 commits July 21, 2025 13:43
…for loading and error styling using css data-state
@Copilot Copilot AI review requested due to automatic review settings September 4, 2025 10:55
@rolandVi rolandVi requested a review from a team as a code owner September 4, 2025 10:55
@github-actions github-actions bot added the area-blazor Includes: Blazor, Razor Components label Sep 4, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Sep 4, 2025
@rolandVi rolandVi changed the title Roland/media component Blazor Media Component Suite Sep 4, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a comprehensive media component suite for efficiently handling non-HTTP sources (byte[]s or streams) with streaming through JS interop and browser-side caching. The suite provides Image, Video, and FileDownload components built on a shared foundation with unified source handling, automatic caching, and stream-based transfer for optimal performance.

Key changes include:

  • New media component architecture with MediaSource class and abstract MediaComponentBase
  • Browser Cache Storage API integration for efficient content reuse
  • TypeScript implementation for streaming and download functionality with fallback support
  • Comprehensive test coverage including unit tests and E2E tests

Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Components/Web/src/Media/ Core media component implementation including base classes and concrete components
src/Components/Web.JS/src/Rendering/BinaryMedia.ts TypeScript implementation for streaming, caching, and download functionality
src/Components/test/testassets/BasicTestApp/MediaTest/ E2E test components for validating media component behavior
src/Components/test/E2ETest/Tests/ E2E test suites for Image, Video, and FileDownload components
src/Components/Web/test/Media/ Unit tests for media component functionality
src/Components/Web/src/PublicAPI.Unshipped.txt Public API surface additions

rolandVi and others added 2 commits September 4, 2025 13:16
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a great start, but I think we need to tweak things a bit to balance code-reuse and public API surface.

Comment on lines 50 to 80
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, TagName);

builder.AddAttribute(1, MarkerAttributeName, "");

if (IsLoading)
{
builder.AddAttribute(2, "data-state", "loading");
}
else if (_hasError)
{
builder.AddAttribute(2, "data-state", "error");
}

builder.AddAttribute(3, "href", "javascript:void(0)");

builder.AddAttribute(4, "onclick", EventCallback.Factory.Create(this, OnClickAsync));

if (AdditionalAttributes?.ContainsKey("href") == true)
{
AdditionalAttributes.Remove("href");
}
builder.AddMultipleAttributes(6, AdditionalAttributes);

builder.AddElementReferenceCapture(7, elementReference => Element = elementReference);

builder.AddContent(8, Text ?? "Download");

builder.CloseElement();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, I think it should be something like <a href="file" download>Text</a>.

Also, if we want to avoid having the text, we can receive RenderFragment<T> as a parameter (where T might be some context we want to make available for rendering). Or just RenderFragment and let the consumer define the UI completely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current approach was made to support the native picker direct-to-file download. Right now when a download is triggered, if the native file picker dialog is available, we stream the data direct-to-file. If not, we default to the <a href="file" download>Text</a> download

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must have missed that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we want to avoid having the text, we can receive RenderFragment<T> as a parameter (where T might be some context we want to make available for rendering). Or just RenderFragment and let the consumer define the UI completely.

I've implemented the RenderFragment<T> approach with MediaContext as the context type, giving consumers access to loading states, error conditions and the object url.
The downside is that users have to provide the element reference to make the element propagation to JS possible, by doing something like this:

<Image Context="ctx" Source=...>
    ...
        <img @ref="ctx.Element" src="@ctx.ObjectUrl" .../>
    ...
</Image>

@dariatiurina dariatiurina removed the community-contribution Indicates that the PR has been added by a community member label Sep 4, 2025
@javiercn
Copy link
Member

javiercn commented Sep 5, 2025

@rolandVi to throw a curve ball on this.

@pavelsavara rightly pointed out that for video we can use something like https://developer.mozilla.org/en-US/docs/Web/API/MediaSource which doesn't require us to collect all the chunks in memory, so that's something we would be interested in exploring.

@rolandVi
Copy link
Contributor Author

rolandVi commented Sep 8, 2025

@rolandVi to throw a curve ball on this.

@pavelsavara rightly pointed out that for video we can use something like https://developer.mozilla.org/en-US/docs/Web/API/MediaSource which doesn't require us to collect all the chunks in memory, so that's something we would be interested in exploring.

Thank you for the feedback, this is indeed very useful. I've implemented an approach using the MediaSource api for video , if the api is not available, it fallbacks to the original approach.

Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@javiercn javiercn merged commit 43b81a9 into dotnet:main Sep 17, 2025
30 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the 11.0-preview1 milestone Sep 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants