dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
21.63k stars 1.62k forks source link

[Android] Maui Blazor Hybrid <video> can't display fullscreen #22049

Open MariovanZeist opened 2 weeks ago

MariovanZeist commented 2 weeks ago

Description

When trying to run a <video > the fullscreen option is greyed out, image

Steps to Reproduce

1.> Create a new ".NET MAUI Blazor Hybrid App" project 2.> Add the following code to the "Home.razor" page

<div>
    <video width="100%" controls autoplay>
        <source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
    </video>
</div>

3.> Deploy and run on an Android emulator.

Although this issue is related to https://github.com/dotnet/maui/issues/8030 It won't be fixed by https://github.com/dotnet/maui/pull/15472

As Blazor has its own WebChromeClient implementation.

Link to public reproduction project repository

https://github.com/MariovanZeist/MauiHybridAndroidVideo

Version with bug

8.0.21 SR4.1

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 34

Did you find any workaround?

I have created the following workaround, based on https://github.com/dotnet/maui/pull/15472 (This workaround is also available in the repro, when uncommenting https://github.com/MariovanZeist/MauiHybridAndroidVideo/blob/38fdb32a2773917968ce43c680325dc690d3f262/MauiHybridAndroidVideo/MauiProgram.cs#L23-L26)

Create an Android platform specific file called BlazorWebChromeClient.cs

using Android.App;
using Android.Content;
using Android.OS;
using Android.Views;
using Android.Webkit;
using Android.Widget;
using AndroidX.Core.View;
using Microsoft.AspNetCore.Components.WebView.Maui;
using Microsoft.Maui.Platform;
using File = Java.IO.File;
using Uri = Android.Net.Uri;
using View = Android.Views.View;
using WebView = Android.Webkit.WebView;

namespace MauiHybridAndroidVideo.Android;

internal class BlazorWebChromeClient : WebChromeClient
{
    WeakReference<Activity> _activityRef;
    View? _customView;
    ICustomViewCallback? _videoViewCallback;
    int _defaultSystemUiVisibility;
    bool _isSystemBarVisible;

    //The code is based on the pull request by jsuarezruiz https://github.com/dotnet/maui/pull/15472

    public BlazorWebChromeClient(BlazorWebViewHandler webViewHandler)
    {
        var activity = (webViewHandler?.MauiContext?.Context?.GetActivity()) ?? Platform.CurrentActivity;
        _activityRef = new WeakReference<Activity>(activity);
    }

    public override void OnShowCustomView(View? view, ICustomViewCallback? callback)
    {
        if (_customView is not null)
        {
            OnHideCustomView();
            return;
        }

        _activityRef.TryGetTarget(out Activity context);

        if (context is null)
            return;

        _videoViewCallback = callback;
        _customView = view;
        context.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Landscape;

        // Hide the SystemBars and Status bar
        if (OperatingSystem.IsAndroidVersionAtLeast(30))
        {
            context.Window.SetDecorFitsSystemWindows(false);

            var windowInsets = context.Window.DecorView.RootWindowInsets;
            _isSystemBarVisible = windowInsets.IsVisible(WindowInsetsCompat.Type.NavigationBars()) || windowInsets.IsVisible(WindowInsetsCompat.Type.StatusBars());

            if (_isSystemBarVisible)
                context.Window.InsetsController?.Hide(WindowInsets.Type.SystemBars());
        }
        else
        {
#pragma warning disable CS0618 // Type or member is obsolete
            _defaultSystemUiVisibility = (int)context.Window.DecorView.SystemUiVisibility;
            int systemUiVisibility = _defaultSystemUiVisibility | (int)SystemUiFlags.LayoutStable | (int)SystemUiFlags.LayoutHideNavigation | (int)SystemUiFlags.LayoutHideNavigation |
                (int)SystemUiFlags.LayoutFullscreen | (int)SystemUiFlags.HideNavigation | (int)SystemUiFlags.Fullscreen | (int)SystemUiFlags.Immersive;
            context.Window.DecorView.SystemUiVisibility = (StatusBarVisibility)systemUiVisibility;
#pragma warning restore CS0618 // Type or member is obsolete
        }

        // Add the CustomView
        if (context.Window.DecorView is FrameLayout layout)
            layout.AddView(_customView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent));
    }

    public override void OnHideCustomView()
    {
        _activityRef.TryGetTarget(out Activity context);

        if (context is null)
            return;

        context.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Portrait;

        // Remove the CustomView
        if (context.Window.DecorView is FrameLayout layout)
            layout.RemoveView(_customView);

        // Show again the SystemBars and Status bar
        if (OperatingSystem.IsAndroidVersionAtLeast(30))
        {
            context.Window.SetDecorFitsSystemWindows(true);

            if (_isSystemBarVisible)
                context.Window.InsetsController?.Show(WindowInsets.Type.SystemBars());
        }
        else
#pragma warning disable CS0618 // Type or member is obsolete
            context.Window.DecorView.SystemUiVisibility = (StatusBarVisibility)_defaultSystemUiVisibility;
#pragma warning restore CS0618 // Type or member is obsolete

        _videoViewCallback?.OnCustomViewHidden();
        _customView = null;
        _videoViewCallback = null;
    }

    //Below is code that is copied from https://github.com/dotnet/maui/blob/main/src/BlazorWebView/src/Maui/Android/BlazorWebChromeClient.cs
    //As the class is internal I cannot override it.

    public override bool OnCreateWindow(WebView? view, bool isDialog, bool isUserGesture, Message? resultMsg)
    {
        if (view?.Context is not null)
        {
            // Intercept _blank target <a> tags to always open in device browser
            // regardless of UrlLoadingStrategy.OpenInWebview
            var requestUrl = view.GetHitTestResult().Extra;
            var intent = new Intent(Intent.ActionView, Uri.Parse(requestUrl));
            view.Context.StartActivity(intent);
        }
        // We don't actually want to create a new WebView window so we just return false 
        return false;
    }

    public override bool OnShowFileChooser(WebView? view, IValueCallback? filePathCallback, FileChooserParams? fileChooserParams)
    {
        if (filePathCallback is null)
        {
            return base.OnShowFileChooser(view, filePathCallback, fileChooserParams);
        }
        InternalCallFilePickerAsync(filePathCallback, fileChooserParams);
        return true;
    }

    public static async void InternalCallFilePickerAsync(IValueCallback filePathCallback, FileChooserParams? fileChooserParams)
    {
        try
        {
            await CallFilePickerAsync(filePathCallback, fileChooserParams).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
#if DEBUG
            throw;
#endif
        }
    }

    private static async Task CallFilePickerAsync(IValueCallback filePathCallback, FileChooserParams? fileChooserParams)
    {
        var pickOptions = GetPickOptions(fileChooserParams);
        var fileResults = fileChooserParams?.Mode == ChromeFileChooserMode.OpenMultiple ?
                await FilePicker.PickMultipleAsync(pickOptions) :
                new[] { (await FilePicker.PickAsync(pickOptions))! };

        if (fileResults?.All(f => f is null) ?? true)
        {
            // Task was cancelled, return null to original callback
            filePathCallback.OnReceiveValue(null);
            return;
        }

        var fileUris = new List<Uri>(fileResults.Count());
        foreach (var fileResult in fileResults)
        {
            if (fileResult is null)
            {
                continue;
            }

            var javaFile = new File(fileResult.FullPath);
            var androidUri = Uri.FromFile(javaFile);

            if (androidUri is not null)
            {
                fileUris.Add(androidUri);
            }
        }

        filePathCallback.OnReceiveValue(fileUris.ToArray());
        return;
    }

    private static PickOptions? GetPickOptions(FileChooserParams? fileChooserParams)
    {
        var acceptedFileTypes = fileChooserParams?.GetAcceptTypes();
        if (acceptedFileTypes is null ||
            (acceptedFileTypes.Length == 1 && string.IsNullOrEmpty(acceptedFileTypes[0])))
        {
            return null;
        }

        var pickOptions = new PickOptions()
        {
            FileTypes = new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
                {
                    { DevicePlatform.Android, acceptedFileTypes }
                })
        };
        return pickOptions;
    }
}

To use it call

#if ANDROID
  BlazorWebViewHandler.BlazorWebViewMapper.ModifyMapping(nameof(IBlazorWebView), (handler, view, args) =>
  {
    handler.PlatformView.SetWebChromeClient(new BlazorWebChromeClient(handler));
  });
#endif

In your MauiProgram after builder.Services.AddMauiBlazorWebView();

Relevant log output

No response

ninachen03 commented 2 weeks ago

Verified this issue with Visual Studio 17.10.0 Preview 5 ( 8.0.21 & 8.0.7).I can repro this issue image

jfversluis commented 2 weeks ago

Thanks for the report and workaround. There is already a PR open for this and issues that describe this so it should hopefully be merged soon and in the meantime the workaround can be used. Closing this one as a duplicate.

jfversluis commented 2 weeks ago

Duplicate of #8030

MariovanZeist commented 2 weeks ago

@jfversluis I appreciate your response, but as stated above

Although this issue is related to https://github.com/dotnet/maui/issues/8030 It won't be fixed by https://github.com/dotnet/maui/pull/15472

The pull request by @jsuarezruiz will fix the normal MauiWebChromeClient used in a (not blazor) MAUI apps, But it won't fix the issue when using MAUI Blazor Hybrid, as they both have different implementations of WebChromeClient BlazorWebChromeClient

jfversluis commented 2 weeks ago

Ah missed that bit, thank you!