googleads / videojs-ima

IMA SDK Plugin for Video.js
Apache License 2.0
450 stars 284 forks source link

Browsers where ima3.js re-uses the <video> fail to start HLS videos after preroll ad break. #964

Closed chwagssd closed 3 years ago

chwagssd commented 3 years ago

When using HLS playlist, PreRoll ads show, but leave the player in a broken state. The plugin thinks its still in an ad break player.ads.isAdPlaying() === true and the HLS playlist never gets restored. The same behavior is observed for MidRoll.

For anyone trying to reproduce this issue without a smart tv (i.e Vizio/Hisense/Samsung TIzen TV - happens on all of them), all you need to do is run Chrome with the a user agent string that contains "smartTv", then when you hit an ad break, you can see document.querySelectorAll('video').length is 1 instead of the usual 2 or 3. Normally video ads are played in a separate <video> placed over the content video. But ima sdk re-uses the single

vizio-720p-FW/4.40.21 Model/D24f-F1) smartTv image

Google IMA SDK will re-use a video player on smart tv platforms. I am able to get VideoJS working with the IMA SDK when I use a custom ad integration and connect Google IMA SDK (ima3.js) to VideoJS manually, handling the CONTENT_PAUSE_REQUESTED and CONTENT_RESUME_REQUESTED events, at least for pre-roll. Using this videojs-ima plugin does not work for pre-roll, mid-roll, etc.

The docs for videojs-ima and videojs-contrib-ads both indicate that they will work on platforms where they reuse the single VideoJS player that content is playing.

Steps to reproduce:

  1. Using Chrome, go to the latest codepen example from the videojs-ima documentation, at the current time: https://codepen.io/imasdk/pen/wpyQXP
  2. Open dev tools in Chrome and choose the mobile phone icon to the left of the ELEMENTS tab and then in the drop down select "EDIT", and add a device with user agent "vizio-720p-FW/4.40.21 Model/D24f-F1) smartTv". The other fields don't matter.
  3. Reload the page so the user agent is available for the lib on load
  4. Change the in the example to an HLS playlist, for example <source src="https://d2s50nsq1evnpc.cloudfront.net/video/OTT/hls/Global_WeatherAcrossAmerica/1609461864/manifest.m3u8" type="application/x-mpegURL" />
  5. Use the TAB key if your mouse isn't working in codepen to focus on the play icon in the player, and hit SPACEBAR (or click play)

Expected result:

preroll plays, and then HLS playlist plays

Actual Result:

  1. 1st, an error when the preroll begins playing because the http-streaming plugin is still appending to the buffer
    video.min.js:13 Uncaught DOMException: Failed to read the 'buffered' property from 'SourceBuffer': This SourceBuffer has been removed from the parent media source.
    at Object.get (https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:191064)
    at https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:188584
    at r.get (https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:189094)
    at r.value (https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:208629)
    at i.value (https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:216002)
    at i.value (https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:219395)
    at i.value (https://googleads.github.io/videojs-ima/node_modules/video.js/dist/video.min.js:13:218989)
  2. Preroll plays successfully and beacons fire (quartile/etc)
  3. Preroll end and player restores HLS playback, but of course, uses the "blob://..." object URL. It seems to me the restore approach for this advertising plugin when only 1 player exists for both content + ads is just setting src=

iframeConsoleRunner-7f4d47902dc785f30dedcac9c996b9f31d4dfcc33567cc48f0431bc918c2bf05.js:1 VIDEOJS: ERROR: (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED) The media could not be loaded, either because the server or network failed or because the format is not supported. Tt {code: 4, message: "The media could not be loaded, either because the …rk failed or because the format is not supported."}



Why folks expect this is a supported use case, even though in other tickets back in 2018 and more recently have comments suggesting it is not:

- https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/preload
  - > On the Smart TVs we know that IMA SDK support is included with ima3.js because it is specifically called out on the compatibility table and mentioned as not supporting preloading
- https://github.com/videojs/videojs-contrib-ads
  - > Player state is automatically restored after ad playback, even if the ad played back in the content's video element.

## Versions
- "video.js": "^7.10.2",
- "@videojs/http-streaming": "^2.3.0",
- "@videojs/vhs-utils": "^2.2.1",
- "videojs-contrib-ads": "^6.7.0",
- "videojs-ima": "^1.9.0",

This issue is not new, and goes back to at least 2019 when I started using VideoJS, which is why I've never used the videojs-ima plugin. But I think it is time to isolate the issue. If I can help please let me know. Hopefully the format of this bug is correct. Thank you!
Kiro705 commented 3 years ago

Hello @chwagssd ,

Thank you for raising this issue. Smart TVs are not official supported by the IMA SDK but are part of a list of devices not officially supported. For that reason, and because there is a work around to use IMA without the videoJS-IMA plugin on smart TVs, this issue will be low priority for the IMA team to work on. In addition, the team does not have these devices to test the changes on.

However, because the videoJS-IMA plugin, if you or your team is able to work on a pull request to resolve the issue, I am happy to review the changes and release a new version of the plugin.

Feel free to reply here and I am happy to assist how I can.

Thank you, Jackson IMA SDK DevRel

chwagssd commented 3 years ago

Hi @Kiro705 thank you for the response and direction on the issue. I was able to isolate the primary issue, which is a race condition, where the IMA SDK is starting preroll ads before the readyforpreroll event is fired by videojs-contrib-ads:

https://videojs.github.io/videojs-contrib-ads/integrator/api.html readyforpreroll (EVENT) – Indicates that your ad plugin may start a preroll ad break by calling startLinearAdMode.

What this results in is the videojs-contrib-ads plugin being unable to restore the playback because the state is messed up.

Here is a workaround I found that works for Vizio/HiSense:

1. Configure IMA plugin and ads plugin to not preload and to leave the restore up to the videojs-contri-ads plugin (not the IMA SDK)

const IMA_OPTIONS = {
  timeout: 2000,
  autoPlayAdBreaks: false,
  showControlsForJSAds: true,
  prerollTimeout: 5000,
  vastLoadTimeout: 5000,
  preventLateAdStart: false,
  enablePreloading: false,
  showCountdown: true,
  adsRenderingSettings: {
    restoreCustomPlaybackStateOnAdBreakComplete: false,
  },
  videojsContribAds: {
    stitchedAds: true, // using the same video player, not a 2nd
    restorePlayerSnapshot: true,
  },
}

2. When loading a video, assuming autoplay: true, manually start the ad break

let allowedToShowPreroll = false;

// Will be called to tell videojs-contrib-ads that we are in linear ad mode AFTER ad break starts
const startLinearAdMode = () => {
  console.log('startLinearAdMode()');
  if (!p.ads.inAdBreak()) {
    console.log('VideoPlayer::startLinearAdMode() - calling player.ads.startLinearAdMode()');
    p.ads.startLinearAdMode();
  } else {
    console.log('startLinearAdMode() - already inAdBreak(), not calling player.ads.startLinearAdMode()');
  }
};

// Manual callback for IMA to use when ad break is ready, instead of auto showing ads
const adBreakReadyListener = (evt) => {
    console.log('adBreakReadyListener', evt,' in ad break? ', p.ads.inAdBreak(), allowedToShowPreroll);

    // we are showing before the 'readyforpreroll' fired, because videojs-imasdk 
    // plugin doesn't follow videojs-contrib-ads required plugin 
    // order of operations of waiting for this event to begin playback
    if (!allowedToShowPreroll) {
      p.one('readyforpreroll', startLinearAdMode);
    }
    p.ima.playAdBreak();
}

// Call this to play a video with ads
const loadVideoWithAds = async (src, vmapAdUrl) () => {

  // reset flag
  allowedToShowPreroll = false; // since videojs-ima is not waiting as it should

  // pre fetch the VAST XML so that the player can synchronously have it when 
  // src is set, not required if using p.ima.setContentWithAdTag(src, adTagUrl)
  const xml = await ky.get( vmapAdUrl ).text();

  // 1st video needs to initialize ima() and show ad
  if (!p.ima.initializeAdDisplayContainer) {
        p.on('readyforpreroll', () => { allowedToShowPreroll = true; });

        p.ima(IMA_OPTIONS);
        p.ima.setAdBreakReadyListener(adBreakReadyListener);
        p.ima.initializeAdDisplayContainer();
        p.ima.controller.playerWrapper.seekContentToZero = () => {};
        console.log('initialized ads plugin and now going to load ', src);
        p.ima.setContentWithAdsResponse(src, xml);

  //2nd+ video can use ima, it won't start loading an ad unless you tell it to by resetting the ad tag and calling requestAds()
  } else {
        p.ima.changeAdTag(null);
        p.ima.setContentWithAdsResponse(src, xml);
        p.one('loadedmetadata', () => {
          p.ima.requestAds(); // after video is queued up, get ad requests fired off against the xml
        });
  }

}

Next steps

This type of bootstrapping is complex to go about as I did above, but the reason I did it that way was to make sure that the various plugins interacted in a predictable way when there is only 1 video player in the page, since as soon as IMA SDK begins playing back an ad in the single <video> before VideoJS Contrib Ads has done its snapshot we get into an unsupported ad plugin state.

I think videojs-ima needs to wait for the 'readyforpreroll' event before it starts playing back the ads, and that might solve it.

Kiro705 commented 3 years ago

Hi @chwagssd ,

Thanks for following up with the work around.

I was looking into where the videojs-ima plugin watches for 'readyforpreroll'. Here in sdk-impl.js ads are played after 'readyforpreroll' if 'autoPlayAdBreaks' is true (it is true by default). Otherwise, ads are started here in sdk-impl.js.

Looking at this, I am unsure what would need to be changed to make sure to wait for the 'readyforpreroll', as it looks like that is already happening.

I don't think the videojs-ima plugin has any specifics to support single video player implementations, so the issue may rest there.

Please let me know if I am missing something.

Thank you, Jackson

chwagssd commented 3 years ago

I reviewed that link to where this.initAdsManager is called when autoPlayAdBreaks is enabled, however, there is another place it happens - perhaps it's getting called earlier when autoPlayAdBreaks is false

The problem I had to protect against was my callback getting called BEFORE the 'readyforpreroll' fired, for example:

adBreakReady
player.on('readyforpreroll', (evt){console.log('now contrib ads plugin says IMA has permission to take over and show ads'); });
player.ima.setAdBreakReadyListener((evt){console.log('Got ad break ready, so I think I can play an ad, did IMA wait to emit this callback until "readyforpreroll"?'); });

Is it possible that the plugin receives the google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED and the google.ima.AdEvent.Type.AD_BREAK_READY event before the player's readyforpreroll event? Because in that case this.playerWrapper.onAdBreakStart() would be called prematurely. It appears like some sort of race condition where the ads manager thinks it's allowed to begin playing the ads, even though 'readyforpreroll' has not yet fired.

aakashsingh-maz commented 3 years ago

@chwagssd I am developing application for samsung(tizen)and LG(webOS) , and I was facing the same issue for content not playing back after the google ads. This solution seem to worked for me https://github.com/googleads/videojs-ima/issues/601

var _currentAd

player.ima.addEventListener(google.ima.AdEvent.Type.STARTED, onAdStarted)
player.ima.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdComplete)

function onAdStarted (ev) {
  _currentAd = ev.getAd()
}
function onAdComplete () {
  if (options.forceReload) { // only if we decide it's needed
    var adPod = _currentAd.getAdPodInfo()
    var podInfo = {
      podPosition: adPod.getAdPosition(),
      podLength: adPod.getTotalAds()
    }
    _forceReload(podInfo)
  }
  _currentAd = null
}

function _forceReload (podInfo) {
  if (podInfo.podPosition < podInfo.podLength) { return }
  // _getSrc() returns something like { url: 'https://...', type: 'video/mp4' } which is the exact same value as originally used to play the content
  player.src(_getSrc())
  setTimeout(function () { // might not be needed
    player.play()
  }, 200) // arbitrary
}
rajat-maz commented 1 year ago

Hi @Kiro705, I am facing a similar issue but this time error is being thrown before an ad is completed. In a 15 seconds ads, the error is being thrown within 5-8 seconds.

More info can be found https://github.com/videojs/video.js/issues/8238

Jerome-lara commented 7 months ago

Hi @chwagssd , I encountered the same problem as you. I replaced it with m3u8 video. On certain devices such as the iPhone, the video could not be played after the pre-roll Ads, and error 4 was prompted.