Why is playVideo() not in the code?

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.