maui: HTML5 video not working on MAUI WebView

Description

If I want to add video in my MAUI Blazor App I have a problem with rendering this video on iOS. On Android works well. I think the problem is in BlazorWebView component.

UPDATE This applies to all MAUI webviews, so is not Blazor-specific.

Steps to Reproduce

  1. Create a new blank .NET MAUI Blazor App

  2. Add some sample video mp4 file into the wwwroot/videos folder image

  3. Replace content of Index.razor page with content below:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<video autoplay muted loop playsinline class="slideContent">
    <source src="videos/TRIMOTERM.mp4" type="video/mp4">
</video>
  1. Run app on Android. All good. image

  2. Run app on iPhone 12 (iOS 15.2) and nothing is shown where video could be. image

Version with bug

Preview 13 (current)

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iPhone 12 iOS 15.2, Xcode 13.2.1, VS 17.2.0 Preview 1.0

Did you find any workaround?

No

Relevant log output

Nothing

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 28 (10 by maintainers)

Most upvoted comments

In the meantime, if you’re looking to make this work today, in .NET 7, you can customize the platform view factory or WebViewHandler like this:

#if IOS || MACCATALYST
Microsoft.Maui.Handlers.WebViewHandler.PlatformViewFactory = (handler) =>
{
	var config = Microsoft.Maui.Platform.MauiWKWebView.CreateConfiguration();
	config.AllowsAirPlayForMediaPlayback = true;
	config.AllowsInlineMediaPlayback = true;
	config.AllowsPictureInPictureMediaPlayback = true;
	config.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;
	
	var wv = new Microsoft.Maui.Platform.MauiWKWebView(
		CoreGraphics.CGRect.Empty,
		handler as Microsoft.Maui.Handlers.WebViewHandler,
		config);

	return wv;
};
#endif

Thank you for that! Had a quick look and here is the weird thing… If I add the controls attribute to the video to show the controls and actually click the play button it works just fine. So it seems like there is something preventing it from playing automatically. Not sure if this is a bug on our side then though.

Yeah I tried that as well. And it seems important to set that before you change these values before assigning the config to the WKWebView, so I tried this in my own handler:

public class MyBlazorWebViewHandler : BlazorWebViewHandler
    {
        protected override WKWebView CreateNativeView()
        {
            var webview = base.CreateNativeView();

            var config = webview.Configuration;
            config.AllowsInlineMediaPlayback = true;
            config.MediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypes.None;
            config.MediaPlaybackRequiresUserAction = false;

            return new WKWebView(RectangleF.Zero, config)
            {
                BackgroundColor = UIColor.Clear,
                AutosizesSubviews = true
            };
        }
    }

But it all seemingly has no effect.

One thing that stands out to me is that when I press the play button it loads for a relatively long time, so it seems the video is not even loaded yet. It doesn’t even to attempt to load or autoplay. Not sure what that means, but it might be a hint for someone

For iOS, it looks like we need to set some properties on the configuration object here: https://github.com/dotnet/maui/blob/0a07dab607fba5839ad31d7165bfea070229455d/src/Core/src/Platform/iOS/MauiWKWebView.cs#L108

Unfortunately you need to set these before you pass the configuration object to the web view constructor. Setting them after this will not work.

One idea is to create a builder configuration pattern to specify a delegate and have an opportunity to configure the WKWebViewConfiguration object before it’s used in the ctor: ie: builder.ConfigureWebView(c => AllowsInlineMediaPlayback = true);

Alternatively (or in addition to), we can set some defaults on the web view configuration out of the box:

config.AllowsPictureInPictureMediaPlayback = true;
config.AllowsInlineMediaPlayback = true;
config.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;

There’s more to investigate but that’s the idea.

Hi guys, I’ve banged my head on this issue for the last few days, because I really need my MAUI app to be able to play some videos (in my case the videos are downloaded and served via a custom IFileProvider like the InMemoryFileProvider in the samples, but had the same issues with videos embedded in the APK), but nothing worked. Until half an hour ago, when I’ve had an epiphany: I’ve said to myself “let’s try to get the video through the fetch API, get the blob, then get the corresponding object URL and try to set THAT as the video source”:

fetch('bunny.mp4')
   .then(async r => b = await r.blob())
   .then(b => document.querySelector('video').src = URL.createObjectURL(b));

Well, interestingly enough, this works :\

Checking the requests in DevTools, the only difference between the fetch request and the normal request that the video element does when setting the video in src attribute is that the latter adds these three headers to the request:

Accept-Encoding: identity;q=1, *;q=0
chrome-proxy: frfr
Range: bytes=0-

Range: bytes=0- basically, for what I know, are used by normal webservers for “chunking” the video request, allowing the video element to start the playback earlier, instead of waiting for all the video to have been downloaded. So it seems that the video element is expecting the “web server” to return a correct set of headers to play the video, but this doesn’t happen. With fetch, instead, the whole video is retrieved and the blob object URL is served without the need of chunking.

Since I don’t know if the video element has an attribute that prevents it’s default chunking behavior, the only workaround for this issue that comes to my mind is as follows:

<video id="player" defer-src="bunny.mp4" autoplay loop muted></video>

<script>
   async function loadDeferred() {
      var player = document.querySelector('#player');
      var srcUrl = player.getAttribute('defer-src');
      var r = await (fetch(srcUrl);
      var blob = await r.blob();
      var blobUrl = URL.createObjectURL(blob);
      player.src = blobUrl;
   }
   
   loadDeferred();
</script>

Hope this helps.