Why is playVideo() not in the code?

How come playVideo(); , ` videoPlayer.play(); was not added to this code?

Can it be?

I’m having trouble adding this in.
videoPlayer.play();

I was able to get this far: code still works.
https://jsfiddle.net/ef6u0yt8/4/

 function addPlayer(video, settings) {
    playerVars = Object.assign({
      videoId: video.dataset.id,
      host: "https://www.youtube-nocookie.com",
      events: {
        "onReady": onPlayerReady,
        "onStateChange": onPlayerStateChange
      }
    }, settings);
    players.push(new YT.Player(video, playerVars));

  }

  function play() {
    player.playVideo();
  }
  return {
    addPlayer,
    play
  };
}());

I suspect that it’s because dealing with multiple players gets trickier.
I won’t be able to investigate for a few days as I’m off elsewhere, but will certainly come back to this.

1 Like

There’s no need for it in the code.

Currently the videos are being added when the cover is clicked. That video is set to autoplay, so there’s no need for scripting code to separately play the videos.

If it were a different situation where the video is already without autoplay and a cover is over it, then
the code would use videoPlayer.play()

There’s no problem with combining that play code from other projects. It just won’t get used on this particular page because there’s no situation that uses it.

1 Like

What’s the difference between these?

What does adding null mean?

let player = null;

let player

Adding null is just deliberate to inform people that the variable is supposed to not contain much of anything right then. Without it, it looks like someone just forgot, which is best to be avoided.

1 Like
1 Like

If I remove autoplay, then it can be added to it?

That’s a bad idea, for we would then be putting together our own version of what the api already does for us, Don’t needlessly reinvent the wheel.

1 Like

How come it was added to this code?
https://jsfiddle.net/xtqg3h80/

With autoplay set to 0, after the image is clicked, it will start, just like a regular player.

https://www.sitepoint.com/community/t/how-come-jacketd-requires-a-2nd-clickhandler/370651/37

const cover = document.querySelector(".jacket");

const videoPlayer = (function makeVideoPlayer() {
  "use strict";
  let player = null;

  const tag = document.createElement('script');
  tag.src = "https://www.youtube.com/iframe_api";
  const firstScriptTag = document.getElementsByTagName('script')[0];
  firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

  function onPlayerReady(event) {
    player = event.target;
    player.setVolume(100); // percent
  }

  let hasShuffled = false;

  function onPlayerStateChange(event) {
    const player = event.target;
    const shufflePlaylist = true;

    if (!hasShuffled) {
      player.setShuffle(shufflePlaylist);
      player.playVideoAt(0);
      hasShuffled = true;
    }
  }

  function addPlayer(video) {
    const playlist = "0dgNc5S8cLI,mnfmQe8Mv1g,-Xgi_way56U,CHahce95B1g";
    new YT.Player(video, {

      width: 640,
      height: 360,
      host: "https://www.youtube-nocookie.com",
      playerVars: {
        autoplay: 0,
        controls: 1,
        loop: 1,
        rel: 0,
        iv_load_policy: 3,
        cc_load_policy: 0,
        fs: 0,
        disablekb: 1,
        playlist
      },
      events: {
        "onReady": onPlayerReady,
        "onStateChange": onPlayerStateChange
      }
    });
  }

  function play() {
    player.playVideo();
  }
  return {
    addPlayer,
    play
  };
}());

function onYouTubeIframeAPIReady() {
  const wrapper = cover.nextElementSibling;
  const frameContainer = wrapper.querySelector(".video");
  videoPlayer.addPlayer(frameContainer);
}

(function iife() {
  "use strict";

  function show(el) {
    el.classList.remove("hide");
  }

  function coverClickHandler(evt) {
    const wrapper = evt.currentTarget.nextElementSibling;
    show(wrapper);
    videoPlayer.play();
  }

  cover.addEventListener("click", coverClickHandler);
}());

It was added to that code because it was needed.

With the https://jsfiddle.net/ef6u0yt8/4/ code we can adjust things so that autoplay is not needed on one of the videos, and the need for videoPlayer.play() is there.

Is that what you are after?

1 Like

Yes.

Isn’t that what this does?

  function play() {
    player.playVideo();
  }
  return {
    addPlayer,
    play
  };
}());
function onYouTubeIframeAPIReady() {
    const wrapper = cover.parentElement ;
    const frameContainer = wrapper.querySelector(".video");
    videoPlayer.addPlayer(frameContainer);
}
videoPlayer.play();

Is that though something that you can get going in the https://jsfiddle.net/ef6u0yt8/4/ code?

Meanwhile, I’ll work on what needs to be done there instead.

I was only able to get this far.

https://jsfiddle.net/5p4ug2mn/

  function addPlayer(video, settings) {
    playerVars = Object.assign({
      videoId: video.dataset.id,
      host: "https://www.youtube-nocookie.com",
      events: {
        "onReady": onPlayerReady,
        "onStateChange": onPlayerStateChange
      }
    }, settings);
    players.push(new YT.Player(video, playerVars));

  }

  function play() {
    player.playVideo();
  }
  return {
    addPlayer,
    play
  };
}());

I had trouble with these:

function onYouTubeIframeAPIReady() {
    const wrapper = cover.parentElement ;
    const frameContainer = wrapper.querySelector(".video");
    videoPlayer.addPlayer(frameContainer);
}
videoPlayer.play();

In that case, jacketc looks to be a likely target, for we can add the video player immediately on page load, not with autoplay. and have a click on jacket c hide the jacket and start playing the video.

We want jacketc to not autoplay the video. Other videos on that page autoplay, but with the jacketc one we want to add other code to start that playing separately.

Selectively turn off autoplay

That means first stopping the autoplay on the jacketc video, by setting autoplay to 0 on that jacket.

  loadPlayer({
    target: ".jacketc",
    autoplay: 0,
    width: 600,
    height: 338,
    loop: true,
    playlist
  });

The initPlayer code needs to be updated too, to accept that autoplay value:

  function initPlayer(wrapper) {
...
    // opts.autoplay = 1;
    opts.autoplay = opts.autoplay || 1;
...

That won’t work though when the value is 0, so we need to use a different technique instead.

    opts.autoplay = (
      "autoplay" in opts
      ? opts.autoplay
      : 1
    );

The rest of the initPlayer code should get updated now too, so that any of the parameters can be updated if needed. It would result in a lot of code if we did that with all of them though, so this is where an array comes to the rescue.

The initPlayer code already has some arrays defined, so lets pull those up to the top of the initPlayer function:

  function initPlayer(wrapper) {
    const settingsParams = ["width", "height", "videoid", "host"];
    const playerVarsParams = ["autoplay", "cc_load_policy",
      "controls", "disablekb", "end", "fs", "iv_load_policy",
      "list", "listType", "loop", "playlist", "rel", "start"
    ];
    const video = wrapper.querySelector(".video");

We can now use that playerVarsParams list to check for defined opts values, but we also need a list of default parameters:

    const defaultParams = {
      width: 198,
      height: 198,
      autoplay: 1,
      controls: 1,
      rel: 0,
      enablejsapi: 1,
      iv_load_policy: 3,
      fs: 0,
      disablekb: 1
    };

The opts assignments can now all be replaced with a forEach loop:

    // opts.width = opts.width || 198;
    // opts.height = opts.height || 198;
    // ...
    // opts.disablekb = 1;
    playerVarsParams.forEach(function (param) {
      opts[param] = (
        param in opts
        ? opts[param]
        : defaultParams[param]
      );
    });

That code is a bit too generic to easily understand what it does though, so it gets put into a function so that the function name and parameters help us to understand what it does:

  function updateOpts(opts, params, defaultParams) {
      params.forEach(function (param) {
      opts[param] = (
        param in opts
        ? opts[param]
        : defaultParams[param]
      );
    });
    return opts;
  }
...
    opts = updateOpts(opts, playerVarsParams, defaultParams);

All of those options can now be updated when using loadPlayer to initialize a video, which we have used to turn off autoplay on the jacketc video:

  loadPlayer({
    target: ".jacketc",
    autoplay: 0,
    width: 600,
    height: 338,
    loop: true,
    playlist
  });

Moving code to where it belongs

The videoPlayer code has been modified so much that it’s incapable of adding a video unless a very special set of settings are given to it. That’s too fragile.

We need to update videoPlayer so that the the desired options are given to it, and it then uses those to update a set of playerVars.

That means updating the videoPlayer code so that it has a default set of playerVars, which are updated by the setting that we give it.

I must head off for the day and we are only halfway through, but will return to finish this up.

1 Like

Right now the addPlayer code is very fragile, in that it refuses to work when given no settings. That won’t do. It needs to be much more reliable than that. After fixing that we’ll add settings so that a video can load (but not play) before a button is clicked.

Improve addPlayer

With the addPlayer code, we want it to work properly when no settings are given to it. That means copying the default settings code over to the addPlayer code, with a plan to remove that code from the loadPlayer code.

I’ll also update the updateOpts code to instead be updateParams as that more accurately represents what happens there.

  function updateParams(playerParams, params) {
    const entries = Object.entries(params);
    const paramsToUpdate = entries.forEach(function([key, value]) {
      if (key in playerParams) {
        playerParams[key] = value;
      }
    });
    return playerParams;
  }

The addPlayer code now becomes a lot simpler, with the defaults being updated by values from the settings.

  function addPlayer(video, settings) {
    const playerParamDefaults = {
      videoId: video.dataset.id,
      host: "https://www.youtube-nocookie.com",
      width: 198,
      height: 198,
      playerVars: {
        autoplay: 1,
        controls: 1,
        loop: 0,
        rel: 0,
        enablejsapi: 1,
        iv_load_policy: 3,
        cc_load_policy: 0,
        fs: 0,
        disablekb: 1
      },
      events: {
        "onReady": onPlayerReady,
        "onStateChange": onPlayerStateChange
      }
    };

    const playerParams = updateParams(playerParamDefaults, settings);
    playerParams.playerVars = updateParams(playerParams.playerVars, settings);
    players.push(new YT.Player(video, playerParams));
  }

That is a lot more stable now, and we can remove from the initPlayer code a lot of stuff that’s no longer needed, resulting in initPlayer code that now only consists of:

function initPlayer(wrapper) {
    const video = wrapper.querySelector(".video");
    videoPlayer.addPlayer(video, opts);
  }

That has resulted in the addPlayer code being more robust, and has simplified the initPlayer code too in the process.

Showing the player without playing it

Right now the loadPlayer code doesn’t actually load the player. Instead it sets up a cover click handler that when clicked will load the player. We need to be able to instruct whether a video gets automatically loaded, which happens with jacketc, or whether clicking on the cover loads the player.

We can add an autoload parameter that defaults to false:

  loadPlayer({
    target: ".jacketc",
    autoload: true,
    autoplay: 0,
    ...
  });

The coverClickHandler can now be updated. If autoload is true, we shouldn’t init the player when the cover is clicked.

  function coverClickHandler(evt) {
    const wrapper = evt.currentTarget.nextElementSibling;
    show(wrapper);
    if (!opts.autoload) {
      initPlayer(wrapper);
    }
  }

Before we go much further though, we need to do something about loadPlayer and initPlayer.

  • loadPlayer doesn’t load the player, it initializes things for the player.
  • initPlayer doesn’t init the player. Instead it loads the player.

Those two names need to be switched.

// function loadPlayer(opts) {
function initPlayer(opts) {
...
  // function initPlayer(wrapper) {
  function loadPlayer(wrapper) {
...
  function coverClickHandler(evt) {
    ...
    // initPlayer(wrapper);
    loadPlayer(wrapper);
  }
...
function onYouTubeIframeAPIReady() {
  // loadPlayer({
  initPlayer({

Now the coverClickHandler code makes better sense, where if the autoload has happened we don’t need to load the player.

  function coverClickHandler(evt) {
    const wrapper = evt.currentTarget.nextElementSibling;
    show(wrapper);
    if (!opts.autoload) {
      loadPlayer(wrapper);
    }
  }

Where do we load the autoloaded video from then? It will be after the event handler is assigned.

function initPlayer(opts) {
  ...
  const cover = document.querySelector(opts.target);
  cover.addEventListener("click", coverClickHandler);

  if (opts.autoload) {
    const wrapper = cover.nextElementSibling;
    loadPlayer(wrapper);
  }
}

And as a bit of cleanup, because we have two sets of code using nextElementSibling to get the video, we really should have a small getVideo function too.

  function getWrapper(cover) {
    return cover.nextElementSibling;
  }

  function getVideo(cover) {
    return getWrapper(cover).querySelector(".video");
  }
...
 //  function loadPlayer(wrapper) {
  function loadPlayer(video) {
    // const video = wrapper.querySelector(".video");
    videoPlayer.addPlayer(video, opts);
  }
...
    if (!opts.autoload) {
      const video = getVideo(cover);
      // loadPlayer(wrapper);
      loadPlayer(video);
    }
...
  function coverClickHandler(evt) {
    // const wrapper = evt.currentTarget.nextElementSibling;
    const cover = evt.currentTarget;
    const wrapper = getWrapper(evt.currentTarget);
    show(wrapper);
    if (!opts.autoload) {
      const video = getVideo(wrapper);
      // loadPlayer(wrapper);
      loadPlayer(video);
    }
  }
...
  if (opts.autoload) {
    // const wrapper = cover.nextElementSibling;
    const video = getVideo(cover);
    // loadPlayer(wrapper);
    loadPlayer(video);
  }

We now have code that reliably lets us autoplay videos, and also lets us autoload videos before the cover is clicked.

Now we need the clicking on a cover to trigger the playing of the video, which is the next post.

1 Like

Updating manageCover

Next we need to selectively tell certain covers that they should play the video.

That means updating the manageCover code so that additional things such as playing the video, can be done when the cover is clicked.

That also means moving the manageCover.init code down below all of the functions, so the function we give the cover to play the video, can access the videoPlayer function when initializing the cover.

Currently it’s the equivalent of the following code that’s being used to initialize the cover:

manageCover.init([
  ".jacketc"
]);

We need to give it a callback function. As there are many players we are going to need to tell the videoPlayer which one to use. The players are all added to a player array, so it’s the playerIndex that we give it.

manageCover.init([
  {
    cover: ".jacketc",
    onclick: function (playerIndex) {
      videoPlayer.play(playerIndex);
    }
  }
]);

Get the playerIndex

How are we going to get the player index? Don’t know. Will it work? Don’t know. How is that onclick method run? We can do something about that.

The manageCover code can be updated so that it handles the onclick method.

  function coverClickHandler(evt) {
    const cover = evt.currentTarget;
    hide(cover);
    ...
    if (cover.coverOpts.onclick) {
      cover.coverOpts.onclick(player);
    }
  }

We are now using cover.coverOpts a lot in that handler, so it’s time to assign that to a local variable:

  function coverClickHandler(evt) {
    const cover = evt.currentTarget;
    const opts = cover.coverOpts;
    hide(cover);
    if (opts.show) {
      show(document.querySelector(opts.show));
    }
    if (opts.hide) {
      hide(document.querySelector(opts.hide));
    }
    if (opts.onclick) {
      opts.onclick(playerIndex);
    }
  }

The last thing on our todo list is to get the playerIndex. We can have the addPlayer code give us the index of the player that it added to the list of players.

  function addPlayer(video, settings) {
...
    players.push(new YT.Player(video, playerParams));
    const playerIndex = players.length - 1;
    return playerIndex;
  }

Add playerIndex to cover

But, how do we get that information to the cover, without the cover knowing about the addPlayer code and without the cover code knowing about addPlayer?

We can add that playerIndex to the cover element itself, and have the cover retrieve it from there.

    if (!opts.autoload) {
      const video = getVideo(cover);
    // loadPlayer(video);
      cover.playerIndex = loadPlayer(video);
    }
...
  if (opts.autoload) {
    const video = getVideo(cover);
    // loadPlayer(video);
    cover.playerIndex = loadPlayer(video);
  }

And, because the same code is being used in those if statements, we should move it out to a separate function too.

We can do that by updating loadPlayer to use the cover instead.

  // function loadPlayer(video) {
  function loadPlayer(cover) {
    const video = getVideo(cover);
    cover.playerIndex = videoPlayer.addPlayer(video, opts);
  }
...
    if (!opts.autoload) {
      // const video = getVideo(cover);
      // cover.playerIndex = loadPlayer(video);
      loadPlayer(cover);
    }
...
  if (opts.autoload) {
    // const video = getVideo(cover);
    // cover.playerIndex = loadPlayer(video);
    loadPlayer(cover);
  }

Connect things together

We can now connect things together up in the cover code, and give that playerIndex to the onclick function:

    if (opts.onclick) {
      opts.onclick(cover.playerIndex);
    }

and in the videoPlayer code, we can use that playerIndex to use the correct player:

  // function play() {
  function play(playerIndex) {
    // player.playVideo();
    players[playerIndex].playVideo();
  }

The updated code that we have is found at https://jsfiddle.net/pmw57/94L3ra28/

While making the above improvements some other issues were spotted in regard to the players and playerVars variables, which I’ll delve into next time.

1 Like

I have a question, for this version of it, if the manageCover functions were kept at the top, how might it be added to that?

https://jsfiddle.net/4ry0o976/

Would it be more complicated? or maybe it would be easier, I’m not sure.

Looking at that code, it doesn’t look like you can take one piece and place it into another.

Code Updated:

I was able to get this far without any issues.

https://jsfiddle.net/4ry0o976/2/

const videoPlayer = (function makeVideoPlayer() {
  "use strict";
  const players = [];
  let playerVars = {};
  let player = null;

  const tag = document.createElement('script');
  tag.src = "https://www.youtube.com/player_api";
  const firstScriptTag = document.getElementsByTagName('script')[0];
  firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

  function onPlayerReady(event) {
    const player = event.target;
    player.setVolume(100); // percent
  }

  let hasShuffled = false;

  function onPlayerStateChange(event) {
    const player = event.target;
    if (!hasShuffled) {
      player.setShuffle(true);
      player.playVideoAt(0);
      hasShuffled = true;
    }
    if (event.data === YT.PlayerState.PLAYING) {
      for (let i = 0; i < players.length; i++) {
        if (players[i] !== event.target) players[i].pauseVideo();
      }
    }

    if (playerVars.loop && event.data === YT.PlayerState.ENDED) {
      player.seekTo(playerVars.start);
    }
  }

  function updateParams(playerParams, params) {
    const entries = Object.entries(params);
    const paramsToUpdate = entries.forEach(function([key, value]) {
      if (key in playerParams) {
        playerParams[key] = value;
      }
    });
    return playerParams;
  }

  function addPlayer(video, settings) {
    const playerParamDefaults = {
      videoId: video.dataset.id,
      host: "https://www.youtube-nocookie.com",
      width: 198,
      height: 198,
      playerVars: {
        autoplay: 1,
        controls: 1,
        loop: 0,
        rel: 0,
        enablejsapi: 1,
        iv_load_policy: 3,
        cc_load_policy: 0,
        fs: 0,
        disablekb: 1
      },
      events: {
        "onReady": onPlayerReady,
        "onStateChange": onPlayerStateChange
      }
    };

    const playerParams = updateParams(playerParamDefaults, settings);
    playerParams.playerVars = updateParams(playerParams.playerVars, settings);
    players.push(new YT.Player(video, playerParams));
    return players.length - 1;
  }

  function play(playerIndex) {
    players[playerIndex].playVideo();
  }
  return {
    addPlayer,
    play
  };
}());

function initPlayer(opts) {
  "use strict";

  function show(el) {
    el.classList.remove("hide");
  }

  function getWrapper(cover) {
    return cover.nextElementSibling;
  }

  function getVideo(cover) {
    return getWrapper(cover).querySelector(".video");
  }

  function loadPlayer(cover) {
    const video = getVideo(cover);
    cover.playerIndex = videoPlayer.addPlayer(video, opts);
  }

  function coverClickHandler(evt) {
    const cover = evt.currentTarget;
    const wrapper = getWrapper(cover);
    show(wrapper);
    if (!opts.autoload) {
      loadPlayer(cover);
    }
  }
  const cover = document.querySelector(opts.target);
  cover.addEventListener("click", coverClickHandler);

  if (opts.autoload) {
    loadPlayer(cover);
  }
}
const playlist = "0dgNc5S8cLI,mnfmQe8Mv1g,-Xgi_way56U,CHahce95B1g";

function onYouTubeIframeAPIReady() {
 initPlayer({
    target: ".jacket-left",
    width: 277,
    height: 207
  });

  initPlayer({
    target: ".jacket-middle",
    width: 277,
    height: 207,
    start: 4
  });
  initPlayer({
    target: ".jacket-right",
    width: 277,
    height: 207
  });
  initPlayer({
    target: ".jacketc",
    autoload: true,
    autoplay: 0,
    width: 600,
    height: 338,
    loop: true,
    playlist
  });
  initPlayer({
    target: ".alpha",
    start: 0,
    end: 280,
    loop: true
  });
  initPlayer({
    target: ".beta",
    start: 0,
    end: 240,
    loop: true
  });
  initPlayer({
    target: ".gamma",
    start: 0,
    end: 265,
    loop: true
  });
  initPlayer({
    target: ".delta",
    start: 4,
    end: 254,
    loop: true
  });
  initPlayer({
    target: ".epsilon",
    start: 0,
    end: 242,
    loop: true
  });
  initPlayer({
    target: ".zeta",
    start: 0,
    end: 285,
    loop: true
  });
  initPlayer({
    target: ".eta",
    start: 23,
    end: 312,
    loop: true
  });
  initPlayer({
    target: ".theta",
    start: 2
  });
  initPlayer({
    target: ".iota"
  });
}

No distractions

1 Like

The videoPlayer code has playerVars and player variables

const videoPlayer = (function makeVideoPlayer() {
  "use strict";
  const players = [];
  let playerVars = {};
  let player = null;

Nothing uses the player variable now, because the players one is used instead to manage multiple players.

  // let player = null;

The playerVars one is designed for only one player, but we have multiple players being used here.

We could expand playerVars to be an array, which would mean using playerIndex with that too.

Here is the existing onPlayerStateChange function:

  function onPlayerStateChange(event) {
    const player = event.target;
    if (!hasShuffled) {
      player.setShuffle(true);
      player.playVideoAt(0);
      hasShuffled = true;
    }
    if (event.data === YT.PlayerState.PLAYING) {
      for (let i = 0; i < players.length; i++) {
        if (players[i] !== event.target) players[i].pauseVideo();
      }
    }

    if (playerVars.loop && event.data === YT.PlayerState.ENDED) {
      player.seekTo(playerVars.start);
    }
  }

The onPlayerStateChange function gets the player from the event:

  function onPlayerStateChange(event) {
    const player = event.target;

and here is where there playerVars are being accessed:

    if (playerVars.loop && event.data === YT.PlayerState.ENDED) {
      player.seekTo(playerVars.start);
    }

We don’t need to access playerVars. That is old stale data that related only to the last player to be initialized.

What we want instead is the information that was given in the options when initializing the player. We can save those settings to the player, so let’s get rid of playerVars.

  // let playerVars = {};

We can now add the settings to the player, when we add it to the players array.

  function addPlayer(video, settings) {
    ...
    const playerParams = updateParams(playerParamDefaults, settings);
    playerParams.playerVars = updateParams(playerParams.playerVars, settings);
    const playerIndex = players.length;
    const player = new YT.Player(video, playerParams);
    player.settings = settings;
    players[playerIndex] = player;
    return playerIndex;
  }

In the onPlayerStateChange function we can get the playerIndex by comparing with the current player:

  function getPlayerIndex(player) {
    const playerIndex = players.findIndex(function (thisPlayer) {
      return player === thisPlayer;
    });
    return playerIndex;
  }

  function onPlayerStateChange(event) {
    const playerIndex = getPlayerIndex(event.target);
    const player = players[playerIndex];
    const state = event.data;

In the interest of simplifying things, we can move the rest of the code out to separate functions. The shuffle function is better suited in the onPlayerReady function, and the others are in the onPlayerStateChange function.

  function onPlayerReady(event) {
    const player = event.target;
    shuffle(player);
    player.setVolume(100); // percent
  }
...
  function onPlayerStateChange(event) {
    const playerIndex = getPlayerIndex(event.target);
    const player = players[playerIndex];
    const state = event.data;

    pauseOtherPlayers(player, state);
    restartPlayerWhenEnded(player, state);
  }

With the shuffle code, we can look at the autoplay setting to decide whether to after shuffling the playlist, we should pause the video.

  function shuffle(player) {
    const settings = player.settings;
    if (!settings.playlist) {
      return;
    }
    player.setShuffle(true);
    player.playVideoAt(0);
    if (settings.autoplay === 0) {
      player.pauseVideo();
    }
  }

  function onPlayerReady(event) {
    const player = event.target;
    player.setVolume(100); // percent
    shuffle(player);
  }
...
  // let hasShuffled = false;

The pauseOtherPlayers and restartPlayerWhenEnded functions are all quite straightforward now too.

  function pauseOtherPlayers(player, state) {
    if (state !== YT.PlayerState.PLAYING) {
      return;
    }
    players.forEach(function(thisPlayer) {
      if (thisPlayer !== player) {
        thisPlayer.pauseVideo();
      }
    });
  }

  function restartPlayerWhenEnded(player, state) {
    if (state !== YT.PlayerState.ENDED) {
      return;
    }
    const settings = player.settings;
    if (settings.loop) {
      player.seekTo(settings.start);
    }
  }

The updated code is found at https://jsfiddle.net/8tpyevbc/

That was a lot of work just to add an already loaded and paused video, so that the cover can play that already loaded video. As a benefit though, the code is a lot more resilient and stable than it was before too.