Search code examples
javascriptadsvideo.jsima

Video.js keeps playing after pausing it (while playing ads via IMA)


I use IMA for playing ads in my Video.js player. I have this setup for months and everything was fine until recently. The problem is video and ad are playing at the same time. I pause video before playing ad but it keeps playing. I can only replicate this problem on Chrome on Android but other users claim to have this issue in different scenarios. In production it happens very often but on my CodePen example only sparingly.

What I'm doing exactly: I have two endpoints for VAST responses - one internal source (which is often empty) and one external which I use when internal one fails. I need to request them from my domain so I manually fetch this endpoints and put response into IMA.

It goes like like this:

  1. User presses play or autoplay happens
  2. Video is paused - the issue is here, sometimes it keeps playing
  3. First VAST is fetch but it's empty
  4. Second VAST is fetched and put into IMA
  5. Ad is playing
  6. Ad is finished and video is resumed - when problem happens video is already in progress

I tried manually checking if ad is playing and pausing video if so but it doesn't fix the problem and breaks playback on iOS.

Here is example: https://codepen.io/davlasq/pen/RwOpJJV

<html>

<head>
  <link href="https://unpkg.com/[email protected]/dist/video-js.css" rel="stylesheet">
  <link href="https://unpkg.com/[email protected]/dist/videojs.ima.css" rel="stylesheet">
  <meta charset="utf-8">
  <title>Video.js Starter Template</title>
</head>

<body>
  <video id="my_video" class="video-js" controls width="300">
    <source src="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" type="application/x-mpegURL">
  </video>
  <script src="//imasdk.googleapis.com/js/sdkloader/ima3.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/video.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/videojs.ads.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/videojs.ima.js"></script>
</body>

</html>

const Plugin = videojs.getPlugin("plugin");

// without initializing plugins in setTimeout they don't work for some reason
// so I created this wrapper
class Delay extends Plugin {
  constructor(player, options) {
    super(player, options);

    setTimeout(() => {
      Object.keys(options).forEach((pluginName) => {
        if (pluginName === "ima" && typeof google === "undefined") {
          return;
        }
        player[pluginName](options[pluginName]);
      });
    });
  }
}

videojs.registerPlugin("delay", Delay);

class MyPlugin extends Plugin {
  constructor(player, options) {
    super(player, options);
    this.options = options;
    this.setAds();
  }

  setAds() {
    if (
      !this.options.ads?.vast ||
      !this.player.ima ||
      typeof google === "undefined" ||
      !google?.ima
    ) {
      return;
    }

    this.player.one("play", () => {
      this.playAds(this.options.ads.vast, () => {
        if (this.options.ads.vast_programmatic) {
          this.playAds(this.options.ads.vast_programmatic);
        }
      });
    });
  }

  async playAds(url, onError) {
    this.player.pause();

    const handleError = () => {
      if (typeof onError === "function") {
        onError();
      } else {
        this.player.play();
      }
    };

    try {
      const vast = await (await fetch(url, { mode: "cors" })).text();

      if (!vast) {
        handleError();
        return;
      }

      this.player.ima.controller.settings.adsResponse = vast;

      this.player.ima.controller.settings.adsManagerLoadedCallback = () => {
        this.player.ima.addEventListener(
          google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
          () => {
            this.player.play();
          }
        );
      };

      this.player.one("adserror", handleError);
      this.player.ima.requestAds();
    } catch (error) {
      handleError();
    }
  }
}

videojs.registerPlugin("myplugin", MyPlugin);

const config = {
  plugins: {
    delay: {
      ima: {},
      myplugin: {
        ads: {
          vast: "https://some.failing.url/", // internal ads (often empty)
          vast_programmatic:
            "https://cdnzone.nuevodevel.com/pub/5.0/e-v-1/vmap_ad_sample.xml" // external ads (used when no internal ads)
        }
      }
    }
  }
};

document.addEventListener("DOMContentLoaded", () => {
  const video = document.querySelector("video");
  videojs(video, config);
});

Solution

  • It's issue with IMA: https://github.com/googleads/videojs-ima/issues/1098 Here is my workaround:

    let tempVolume;
    
    player.ima.addEventListener(google.ima.AdEvent.Type.STARTED, () => {
      const adVideoEl = player.el().querySelector(".ima-ad-container video[src]");
      tempVolume = player.volume();
      player.volume(0);
      adVideoEl.volume = tempVolume;
    });
    
    player.ima.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
        player.currentTime(0);
        player.volume(tempVolume);
        player.src(player.src());
        player.play();
      }
    );
    

    So basically I mute video on ad start, then unmute and reload on ad end.