Search code examples
javascriptknockout.jsyoutubephonejsdevextreme

Binding YouTube Video to Div Element from Seperate JS file


I have this problem embedding YouTube video in a PhoneJS single-page mobile application. In PhoneJS, the JS scripts are defined in a different file. So I defined the HTML div like this:

<div id="player"></div>

Now in the JS file, I did this:

function getVideo() {        
    var tag = document.createElement('script');
    tag.src = "https://www.youtube.com/iframe_api";
    var playerDiv = document.getElementById('player');
    var firstScriptTag = document.getElementsByTagName('script')[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

    var player;
    function onYouTubeIframeAPIReady() {
        player = new YT.Player(playerDiv, {
            height: '250',
            width: '444',
            videoId: sIFYPQjYhv8               
        });
    }        
}

When I run and view the debugger, the call is made to Youtube and response is received, but it is not displayed on the view.

Ok since I am using KnockoutJS binding, I modified the div in the html view like this:

<iframe id="player" type="text/html" width="444" height="250" frameborder="0" data-bind="attr: { src: src }"></iframe>

And then pass in the src video id thus:

src: ko.observable('http://www.youtube.com/embed/' + sIFYPQjYhv8 + '?autoplay=1')

In this case however, in the debugger, the call is not even made to Youtube. Nothing just happens. Actually I prefer to use the API call instead of the second approach.

Any suggestions on how to make the first approach work? I mean using the API call?

EDIT Just want to mention that when I add the code below in the view, the video is streamed alright.

<h1>Video</h1>
        <div id="player"></div>
        <script>
            var tag = document.createElement('script');
            tag.src = "https://www.youtube.com/iframe_api";
            var playerDiv = document.getElementById('player');
            var firstScriptTag = document.getElementsByTagName('script')[0];
            firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

            var player;
            function onYouTubeIframeAPIReady() {
                player = new YT.Player(playerDiv, {
                    height: '250',
                    width: '444',
                    videoId: 'sIFYPQjYhv8'
                });
            }
        </script>

Solution

  • I think the easiest way to do this is to use a custom binding handler with a flag set from the onYouTubeIFrameAPIReady callback

    Sample jsFiddle

    ko.bindingHandlers['player'] = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            // Check if global script and function is declared.
            if ( !document.getElementById('playerScript') ) {
                // Create script
                var tag = document.createElement('script');
                tag.src = "https://www.youtube.com/iframe_api";
                var playerDiv = document.getElementById('player');
                var firstScriptTag = document.getElementsByTagName('script')[0];
                firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    
                // Create global function that their API calls back
                window.playerReady = ko.observable(false);
                window.onYouTubeIframeAPIReady = function() {
                    window.playerReady(true);
                };
            }
        },
        update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    
            var value = valueAccessor(),
                id = value.id(),
                height = ko.unwrap(value.height) || '250',
                width = ko.unwrap(value.width) || '444'
            ;
    
            if ( !value.id()) {
                return;
            }
    
            if ( !window.playerReady() ) {
                // YT hasn't invoked global callback.  Subscribe to update
                var subscription;
                subscription = window.playerReady.subscribe( function(newValue) {
                     if ( newValue ) {
                         subscription.dispose();
                         // Just get this binding to fire again
                         value.id.notifySubscribers(value.id());
                     }
                });
            } else {
    
                var player = new YT.Player( element, {
                    height: height,
                    width: width,
                    videoId: id 
                });
            }
        },
    }
    

    Now change your player div to

    <div data-bind="player: { id: id, height: height, width: width }"></div>
    

    Finally bind

    var vm = { 
        id: 'sIFYPQjYhv8',
        height: '250',
        width: '444'
    };
    ko.applyBindings( vm )
    

    EDIT

    To remove the reliance on window, put your script tag that adds the new script element back, tweek as below, modify their callback and use a setTimeout instead of the "playerReady" observable

    HTML Script

    var tag = document.createElement('script');
    tag.src = "https://www.youtube.com/iframe_api";
    tag.setAttribute('id', 'playerScript');
    tag.setAttribute('data-ready', 'false');
    ...
    
    function onYouTubeIframeAPIReady = function() {
        document.getElementById('playerScript').setAttribute('data-ready', 'true');
    };
    

    Player Binding

        update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    
            var value = valueAccessor(),
                id = value.id(),
                height = ko.unwrap(value.height) || '250',
                width = ko.unwrap(value.width) || '444',
                playerScript = document.getElementById('playerScript')
            ;
    
            if ( !value.id()) {
                return;
            }
    
            if ( !playerScript || playerScript.getAttribute('data-ready') !== 'true' ) ) {
                // YT hasn't invoked global callback.  
                setTimeout( function() {
                     value.id.notifySubscribers(value.id());
                }, 50);
            } else {
    
                var player = new YT.Player( element, {
                    height: height,
                    width: width,
                    videoId: id 
                });
            }
        }