Skip to content

Blazor Media Component Suite - #63540

Merged
javiercn merged 81 commits into
dotnet:mainfrom
rolandVi:roland/media-component
Sep 17, 2025
Merged

Blazor Media Component Suite#63540
javiercn merged 81 commits into
dotnet:mainfrom
rolandVi:roland/media-component

Conversation

@rolandVi

@rolandVi rolandVi commented Sep 4, 2025

Copy link
Copy Markdown
Member

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
@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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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

Comment thread src/Components/Web/src/Media/MediaComponentBase.cs Outdated
Comment thread src/Components/Web/src/Media/FileDownload.cs Outdated
Comment thread src/Components/test/E2ETest/Tests/FileDownloadTest.cs Outdated
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>

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

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 thread src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj Outdated
Comment thread src/Components/Web/src/Media/Image.cs Outdated
Comment thread src/Components/Web/src/Media/MediaComponentBase.cs Outdated
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
Copy Markdown
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
Copy Markdown
Member 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
Copy Markdown
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
Copy Markdown
Member 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

javiercn commented Sep 5, 2025

Copy link
Copy Markdown
Member

@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

rolandVi commented Sep 8, 2025

Copy link
Copy Markdown
Member Author

@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.

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

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.

4 participants