Search code examples
c#winformsyoutubemedia-playerwebview2

How do I get YouTube native player to allow full screen in WebView2?


I am using the WebView2 browser to allow the playback of some YouTube videos, but the Full Screen option is unavailable.
The code I have written is below. I am very new to coding, and I may have gotten it very wrong.
Any more information you need I am willing to provide.

public partial class HowToViewSites: CMBaseDialog
{
    private dynamic player;

    public HowToViewSites()
    {
        InitializeComponent();
        InitializeWebView2();
    }

    private async void InitializeWebView2()
    {
       
        await webView21.EnsureCoreWebView2Async(null);

        webView21.CoreWebView2.Settings.IsScriptEnabled = true;

        webView21.CoreWebView2InitializationCompleted += WebView2_CoreWebView2InitializationCompleted;                     
        
        string htmlContent = @"
            <html>
            <head>
            <!-- Include the YouTube iframe API script using HTTPS -->
            <script src='https://www.youtube.com/iframe_api'></script>
            </head>
            <body>
            <!-- Embed the YouTube video with enablejsapi parameter over HTTPS -->
            <iframe id='player' type='text/html' width='840' height='560'
                src='https://www.youtube.com/embed/QgMnE1No_6A?enablejsapi=1'
                frameborder='0'></iframe>

            <!-- JavaScript code to handle fullscreen changes -->
            <script>
             // Initialize the YouTube iframe API when the script is loaded
            function onYouTubeIframeAPIReady() {
            player = new YT.Player('player', {
            events: {
                    'onReady': onPlayerReady,
                    'onStateChange': onPlayerStateChange
                    }
                });
            }

            function onPlayerReady(event) {
            // Player is ready
            // You can control playback and perform other actions here
            }

            function onPlayerStateChange(event) {
            // Player state has changed (e.g., video started, paused, etc.)
            // Check if the player is in fullscreen mode
                var isFullscreen = document.fullscreenElement === player.getIframe();

                if (isFullscreen) {
            // Trigger the player's native fullscreen mode
                external.RequestFullscreen();
                } else {
            // Exit fullscreen
                external.ExitFullscreen();
             }
            }

            document.addEventListener('fullscreenchange', function () {
            console.log('Fullscreen change event triggered');
            window.chrome.webview.postMessage('fullscreenchange');
            });
            </script>
            </body>
            </html>
        ";
        webView21.NavigateToString(htmlContent);

        webView21.CoreWebView2.Settings.IsWebMessageEnabled = true;
        webView21.WebMessageReceived += CoreWebView2_WebMessageReceived;            
    }

    private async void WebView2_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
    {
        if (e.IsSuccess)
        {
            // No need to manually call onYouTubeIframeAPIReady here
        }
        else
        {
            MessageBox.Show("WebView2 initialization failed.");
        }
    }

    private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
    {
        string message = e.TryGetWebMessageAsString();
        if (message == "enteredFullscreen")
        {
            RequestFullscreen();
        }
        else if (message == "exitedFullscreen")
        {
            ExitFullscreen();
        }
    }
            
    public void RequestFullscreen()
    {
        this.BeginInvoke(new Action(() =>
        {
            this.FormBorderStyle = FormBorderStyle.None;
            this.WindowState = FormWindowState.Maximized;
            this.TopMost = true;
            
            player.playVideo();
        }));
    }

    public void ExitFullscreen()
    {
        this.BeginInvoke(new Action(() =>
        {
            this.FormBorderStyle = FormBorderStyle.Sizable;
            this.WindowState = FormWindowState.Normal;
            this.TopMost = false;
            
            player.pauseVideo();
        }));
    }
}

Thank you so much for taking your time out to help me.


Solution

  • A few notes:

    • The WebView2 initialization is not performed correctly. You should really await your InitializeWebView2() method that should return Task.
      You can override the OnLoad() method (or subscribe to the Load event), make it async, and await InitializeWebView2() there
    • [WebView2].CoreWebView2InitializationComplete needs to be subscribed to before you call .EnsureCoreWebView2Async()
    • The WebView2 Control already knows when an HTML element is requesting full-screen layout. The ContainsFullScreenElementChanged event is raised to notify of a status change. You then check what ContainsFullScreenElement is set to and act accordingly

    I suggest not directly using an IFrame to embed a video.
    The YouTube Player has already this capability: it can replace an element, based on the ID you pass to its constructor, with an IFrame element, configured using the properties you set when the player is created.
    This allows to, e.g., stop / pause / resume the playback calling JavaScript functions. You can do that using the ExecuteScriptAsync() method.
    You can also set enablejsapi = 1 in the playerVars Property of the Player.

    This allows to, for example, copy / acquire the address of a video as it is:

    This is the one you posted:  
    https://www.youtube.com/watch?v=QgMnE1No_6A
    

    and only use its videoId - QgMnE1No_6A - to initialize the Player

    This also enables Full Screen button of the Player.
    If you use an IFrame, you have to set an attribute:

    <iframe allowfullscreen="allowfullscreen" [...] >  </iframe>
    

    Note that I've renamed the Control webView (no reason, it's just what it is :)

    public partial class HowToViewSites: CMBaseDialog
    {
        public HowToViewSites() => InitializeComponent();
    
        protected override async void OnLoad(EventArgs e) {
            base.OnLoad(e);
            await InitializeWebView2();
    
            var videoSource = new Uri("https://www.youtube.com/watch?v=QgMnE1No_6A");
            var videoId = videoSource.Query.Substring(videoSource.Query.LastIndexOf("=") + 1);
            webView.NavigateToString(GetHTMLContent(videoId, new Size(840, 560)));
        }
    
        private async Task InitializeWebView2()
        {
           // See the docs about CoreWebView2EnvironmentOptions, you may need it
           // when your application is deployed. Now it's just set to defaults
            var options = new CoreWebView2EnvironmentOptions(null, null, "", false);
            var env = await CoreWebView2Environment.CreateAsync("", null, options);
    
            webView.CoreWebView2InitializationCompleted += WebView2_CoreWebView2InitializationCompleted;
            await webView.EnsureCoreWebView2Async(env);
        }
    
        private void WebView2_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e) {
            if (e.IsSuccess) {
                // These two settings are just defaults. To clarify the intention 
                webView.CoreWebView2.Settings.IsWebMessageEnabled = true;
                webView.CoreWebView2.Settings.IsScriptEnabled = true;
    
                webView.WebMessageReceived += async (o, a) => {
                    string message = a.TryGetWebMessageAsString();
                    switch (message) {
                        case "Player initialized":
                            // Starts the playback after the YT Player has been initialized
                            await Task.Delay(200);
                            await webView.ExecuteScriptAsync("ResumeVideo()");
                            break;
                        default:
                            Debug.WriteLine(message);
                            break;
                    }
                };
    
                webView.CoreWebView2.ContainsFullScreenElementChanged += (o, a) => {
                    RequestFullscreen(webView.CoreWebView2.ContainsFullScreenElement);
                };
            }
            else {
                MessageBox.Show("Core WebView component initialization failed");
                Application.ExitThread();
            }
        }
    
        public void RequestFullscreen(bool isFullScreen) {
            FormBorderStyle = isFullScreen ? FormBorderStyle.None : FormBorderStyle.Sizable;
            WindowState = isFullScreen ? FormWindowState.Maximized : FormWindowState.Normal;
        }
    }
    

    I've changed the HTML and the JavaScripts, so you can initialize the Player with a Video ID and the page with a specified Size:

    private string GetHTMLContent(string videoId, Size videoSize) {
        var sb = new StringBuilder(@"
            <html>
            <head>
                <!-- Include the YouTube iframe API script using HTTPS -->
                <script src='https://www.youtube.com/iframe_api'></script>
                <script>
                var player;
                function onYouTubeIframeAPIReady() {
                    player = new YT.Player('playerframe', {
                    events: {
                        'onReady': onPlayerReady
                    },
                    videoId: '[VIDEO-ID]',
                    height: '[VIDEO-HEIGHT]',
                    width: '[VIDEO-WIDTH]',
                    playerVars : {
                        'enablejsapi' : 1
                    }});
                }
    
                function StopVideo() { player.stopVideo(); }
                function PauseVideo() { player.pauseVideo(); }
                function ResumeVideo() { player.playVideo(); }
    
                function onPlayerReady(event) {
                    // Player is ready
                    window.chrome.webview.postMessage('Player initialized');
                }
                </script>
            </head>
            <body>
                <!-- Element replaced with an IFrame when the YouType Player is initialized -->
                <div id='playerframe'></div>
            </body>
            </html>
        ");
    
        sb = sb.Replace("[VIDEO-ID]", videoId)
               .Replace("[VIDEO-HEIGHT]", videoSize.Height.ToString())
               .Replace("[VIDEO-WIDTH]", videoSize.Width.ToString());
    
        return sb.ToString();
    }
    

    Now, if you want to interact with the Player, just call ExecuteScriptAsync() to execute a function you have setup. For now, you can start / stop / resume the payback:

    await webView.ExecuteScriptAsync("StopVideo()");
    await webView.ExecuteScriptAsync("PauseVideo()");
    await webView.ExecuteScriptAsync("ResumeVideo()");