loading multiple video players with youtube api

2019-01-08 19:30发布

问题:

I need to load more than one video with youtube's API. This is my first time using it so I'm not sure what I'm doing wrong, but this is what I'm trying:

  var player;
  var player2;
  function onYouTubePlayerAPIReady() {
    player = new YT.Player('player', {
      videoId: 'hdy78ehsjdi'
    });
    player2 = new YT.Player('player', {
      videoId: '81hdjskilct'
    });
  }

回答1:

Since onYouTubeIframeAPIReady function is supposed to called only once the following approach could be used:

  • initialize and save video player information (ControlId,width,height,VideoId) in array

  • call onYouTubeIframeAPIReady function to create all the video players

Example

var playerInfoList = [{id:'player',height:'390',width:'640',videoId:'M7lc1UVf-VE'},{id:'player1',height:'390',width:'640',videoId:'M7lc1UVf-VE'}];

      function onYouTubeIframeAPIReady() {
        if(typeof playerInfoList === 'undefined')
           return; 

        for(var i = 0; i < playerInfoList.length;i++) {
          var curplayer = createPlayer(playerInfoList[i]);
        }   
      }
      function createPlayer(playerInfo) {
          return new YT.Player(playerInfo.id, {
             height: playerInfo.height,
             width: playerInfo.width,
             videoId: playerInfo.videoId
          });
      }


回答2:

The first parameter of new YT.Player needs to be the id of the HTML element (f.e. a DIV) to be replaced with an iframe to the video. As you use 'player' for both of these objects, you will load both into the same element.

<div id="ytplayer1"></div>
<div id="ytplayer2"></div>

<script>
  var player;
  var player2;
  function onYouTubePlayerAPIReady() {
    player = new YT.Player('ytplayer1', {
      height: '390',
      width: '640',
      videoId: 'hdy78ehsjdi'
    });
    player2 = new YT.Player('ytplayer2', {
      height: '390',
      width: '640',
      videoId: '81hdjskilct'
    });
  }
</script>

Parameters of the functions are described in the Youtube API documentation:
https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player (EDIT: changed to the right link)



回答3:

I had a more expansive issue that boiled down to this same problem. The requirements I had were to write a JS class to manage one or more (the number can vary from 1 to infinity) video embeds. The backend system is ExpressionEngine (but that's irrelevant here). The primary goal was to set up a framework for analytics that pushes individual data to our Adobe Analytics platform. Shown here is merely the part that gives play count, it can be expanded a lot from here.

The CMS allows editors to create modules on the page that present a video. One video per module. Each module is basically a section of HTML arranged via Bootstrap 3 (irrelevant for this answer).

The relevant HTML looks like this:

<div id="js_youTubeContainer_{innov_mod_ytplayer:id}" class="embed-responsive embed-responsive-16by9">
  <div id="js_youTubeFrame_{innov_mod_ytplayer:id}" class="embed-responsive-item"></div>
</div>

The part that says "{innov_mod_ytplayer:id}" is the YouTube Video ID from our CMS. This allows for a unique ID for each embeded item. This is important later.

Below this, I then render out:

            var innovYouTube_{innov_mod_ytplayer:id} = new Ariba.Innovations.YouTube.Class({
                'innovYouTubeVideoId': '{innov_mod_ytplayer:id}',
                'innovYouTubeVideoTitle': '{innov_mod_ytplayer:title}',
                'innovYouTubeDivId' : 'js_youTubeFrame_{innov_mod_ytplayer:id}'
            });
            innovYouTube_{innov_mod_ytplayer:id}.Init(); // And... Go!

            var onYouTubeIframeAPIReady = (function() {
                try{ //wrap this in try/catch because it actually throws errors when it runs subsequent times - this is expected as it's related to YouTube "rerunning" the function on other videos.
                    innovYouTube_{innov_mod_ytplayer:id}.config.functionCache = onYouTubeIframeAPIReady; //cache the existing global function
                    return function() {
                        try{
                            innovYouTube_{innov_mod_ytplayer:id}.onYouTubeIframeAPIReady(); //execute this instance's function
                            var newOnYouTubeIframeAPIReady = innovYouTube_{innov_mod_ytplayer:id}.config.functionCache.apply(this, arguments); //add instances to global function
                            return newOnYouTubeIframeAPIReady; //update global function
                        }catch(err){}
                    };
                }catch(err){}
            })();

You'll see some ExpressionEngine template tags here too - those are just the Video ID and the Video Title from YouTube. To replicate this, you'll need to change those of course.

What this does is allow me to dynamically update the single global callback with new code for each newly embedded video. In the end, this callback will contain calls to their own instances of my class. You need those try/catch blocks because it throws a false-positive error for all the "other" embeds except the one it's actually executing "right now" - remember this script runs once for every embed on the page. The errors are expected and actually cause no problem, so the try/catch suppresses them.

Using the CMS template tag, I create each instance based on the YouTube video ID. I would run into a problem if someone added the same video module more than once, but that's a business problem easily handled since that's not supposed to happen. This allows me to instantiate unique instances of my class over and over for each video.

The critical part of that script is based on this extremely helpful SO answer: Adding code to a javascript function programmatically

Here's the actual class. It's commented mostly... We use jQuery, so you'll see one important use of it here in the $.extend() method. I use that as a convenience in the class constructor method, but you could do that with vanilla JS too (JavaScript equivalent of jQuery's extend method) I just find the jQuery easier to read, and since it's available to me, I use it.

if (typeof Ariba === "undefined") { var Ariba = {}; }
if (typeof Ariba.Innovations === "undefined") { Ariba.Innovations = {}; }
if (typeof Ariba.Innovations.YouTube === "undefined") { Ariba.Innovations.YouTube = {}; }

if (typeof Ariba.Innovations.YouTube.Class === "undefined") {//this script may be embedded more than once - do this to avoid re-processing it on subsequent loads
    Ariba.Innovations.YouTube.Class = function (config) {
        this.static = {
            'ytScriptId': 'js_youtubeFrameAPI',
            'ytScriptUrl': 'https://www.youtube.com/iframe_api'
        };//static configuration.  Will overwrite any other settings with the same name
        this.config = {//optional configuration variables. Will be overridden by instance or static settings with the same name.
            'adobeAnalyticsFired': false
        };
        this.config = $.extend(true, this.config, config);//inserts (destructively!) the instance settings.
        this.config = $.extend(true, this.config, this.static);//inserts (destructively!) the static settings.
        this.config.this = this;
    };

    Ariba.Innovations.YouTube.Class.prototype.Init = function () {
        //Note: have to allow it to write it over an over because calling the API script is what makes YouTube call onYouTubeIframeAPIReady.
        //if (document.getElementById('js_youtubeFrameAPI') === null) { // don't add the script again if it already exists!
        this.config.apiScript = document.createElement('script');
        this.config.apiScript.src = 'https://www.youtube.com/iframe_api';
        this.config.apiScript.id = 'js_youtubeFrameAPI' + this.config.innovYouTubeVideoId;
        this.config.firstScriptTag = document.getElementsByTagName('script')[0];
        this.config.firstScriptTag.parentNode.insertBefore(this.config.apiScript, this.config.firstScriptTag);
        //}
        //else { console.log("iframe script already embedded", this.config.innovYouTubeVideoId); }
    }

    Ariba.Innovations.YouTube.Class.prototype.onYouTubeIframeAPIReady = function (event) {
        //console.log("onYouTubeIframeAPIReady", this.config.innovYouTubeVideoId, arguments);
        var _this = this;
        //console.log(this);
        this.config.ytPlayer = new YT.Player(this.config.innovYouTubeDivId, {
            videoId: this.config.innovYouTubeVideoId,
            events: {
                'onReady': _this.onPlayerReady.bind(_this),
                'onStateChange': _this.onPlayerStateChange.bind(_this)
            }
        });
    }

    Ariba.Innovations.YouTube.Class.prototype.onPlayerReady = function (event) {
        //console.log("onPlayerReady", this.config.innovYouTubeVideoId, event);
    }

    Ariba.Innovations.YouTube.Class.prototype.onPlayerStateChange = function (event) {
        //console.log("onPlayerStateChange", this.config.innovYouTubeVideoId, event, this);
        if (event.data === YT.PlayerState.PLAYING && !this.config.adobeAnalyticsFired) {
            //console.log("YouTube Video is PLAYING!!", this.config.innovYouTubeVideoId);
            this.config.adobeAnalyticsFired = true;
            if (typeof _satellite !== "undefined") {
                window._satellite.data.customVars.adhoc_tracker_val = "Innovations Video: " + this.config.innovYouTubeVideoTitle + " (" + this.config.innovYouTubeVideoId + ")";
                _satellite.track('adhoctrack');
            }
        }
    }
}

A few other notes:

Keeping scope in the class instance is easy once you get the main global callback problem solved. You just have to add .bind(). For example:

'onReady': _this.onPlayerReady.bind(_this)

You might also see:

var _this = this;

This is so the "this" scope for the instance isn't lost accidentally. Maybe not necessary, but it's a convention I've adopted over the years.

Anyway, I've been working on this for a week now, and figured I'd share it with the SO community since it's clear from my looking for answers a lot of others have been searching for solutions to this too.



回答4:

I needed this same thing in React. Expanding upon Vadim's answer you could do something like the following and add them to an object then create the player if you don't know what the array of players will look like prior.

const YoutubeAPILoader = {
  _queue: [],
  _isLoaded: false,

  load: function (component) {
    // if the API is loaded just create the player
    if (this._isLoaded) {
      component._createPlayer()
    } else {
      this._queue.push(component)

      // load the Youtube API if this was the first component added
      if (this._queue.length === 1) {
        this._loadAPI()
      }
    }
  },

  _loadAPI: function () {
    // load the api however you like
    loadAPI('//youtube.com/player_api')

    window.onYouTubeIframeAPIReady = () => {
      this._isLoaded = true
      for (let i = this._queue.length; i--;) {
        this._queue[i]._createPlayer()
      }
      this._queue = []
    }
  }
}


回答5:

The HTML

<div data-id="youtubevideoidhere" class="video"></div>
<div data-id="youtubevideoidhere" class="video"></div>
<div data-id="youtubevideoidhere" class="video"></div>

The JS for Videos

// CREATE VIDEOS "CLASS" to handler videos
var Videos = (function() {
    // VARIABLES
    var $   = jQuery,   // The jquery
    players = [],       // players array (to coltrol players individually)
    queue   = [];       // videos queue (once api is ready, transform this into YT player)

    // Constructor
    function Videos() {}

    // METHODS
    // Add elements to queue
    Videos.prototype.add = function($video) {
        queue.push($video);
    };

    // Load YT API
    Videos.prototype.loadApi = function() {
        // jQuery get script
        $.getScript("//www.youtube.com/iframe_api", function() {
            // once loaded, create the onYouTubeIframeAPIReady function
            window.onYouTubeIframeAPIReady = function() {
                queue.forEach(function($video) {
                    // Create the YT player
                    var player = new YT.Player($video.get(0), {
                        'width': "100%",
                        'height': "100%",
                        'videoId': $video.data("id")
                    });
                    // add to players array
                    players.push(player);
                });
            };
        });
    };

    return Videos;

})();

And then, create videos like this

var videos = new Videos();
$('.video').each( function () {
    videos.add( $(this) );
})
videos.loadApi();