UNPKG

videojs-contrib-ads

Version:

A framework that provides common functionality needed by video advertisement libraries working with video.js.

265 lines (222 loc) 8.55 kB
/* The snapshot feature is responsible for saving the player state before an ad, then restoring the player state after an ad. */ import videojs from 'video.js'; let tryToResumeTimeout_; /* * Returns an object that captures the portions of player state relevant to * video playback. The result of this function can be passed to * restorePlayerSnapshot with a player to return the player to the state it * was in when this function was invoked. * @param {Object} player The videojs player object */ export function getPlayerSnapshot(player) { let currentTime; if (videojs.browser.IS_IOS && player.ads.isLive(player)) { // Record how far behind live we are if (player.seekable().length > 0) { currentTime = player.currentTime() - player.seekable().end(0); } else { currentTime = player.currentTime(); } } else { currentTime = player.currentTime(); } const tech = player.$('.vjs-tech'); const tracks = player.textTracks ? player.textTracks() : []; const suppressedTracks = []; const snapshotObject = { ended: player.ended(), currentSrc: player.currentSrc(), sources: player.currentSources(), src: player.tech_.src(), currentTime, type: player.currentType() }; if (tech) { snapshotObject.style = tech.getAttribute('style'); } for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; suppressedTracks.push({ track, mode: track.mode }); track.mode = 'disabled'; } snapshotObject.suppressedTracks = suppressedTracks; return snapshotObject; } /* * Attempts to modify the specified player so that its state is equivalent to * the state of the snapshot. * @param {Object} player - the videojs player object * @param {Object} snapshotObject - the player state to apply */ export function restorePlayerSnapshot(player, callback) { const snapshotObject = player.ads.snapshot; if (callback === undefined) { callback = () => {}; } if (player.ads.disableNextSnapshotRestore === true) { player.ads.disableNextSnapshotRestore = false; delete player.ads.snapshot; callback(); return; } // The playback tech let tech = player.$('.vjs-tech'); // the number of[ remaining attempts to restore the snapshot let attempts = 20; const suppressedTracks = snapshotObject.suppressedTracks; let trackSnapshot; const restoreTracks = function() { for (let i = 0; i < suppressedTracks.length; i++) { trackSnapshot = suppressedTracks[i]; trackSnapshot.track.mode = trackSnapshot.mode; } }; // Finish restoring the playback state. // This only happens if the content video element was reused for ad playback. const resume = function() { let currentTime; // Live video on iOS has special logic to try to seek to the right place after // an ad. if (videojs.browser.IS_IOS && player.ads.isLive(player)) { if (snapshotObject.currentTime < 0) { // Playback was behind real time, so seek backwards to match if (player.seekable().length > 0) { currentTime = player.seekable().end(0) + snapshotObject.currentTime; } else { currentTime = player.currentTime(); } player.currentTime(currentTime); } // iOS live play after restore if player was paused (would not be paused if // ad played muted behind ad) if (player.paused()) { const playPromise = player.play(); if (playPromise && playPromise.catch) { playPromise.catch((error) => { videojs.log.warn('Play promise rejected in IOS snapshot resume', error); }); } } // Restore the video position after an ad. // We check snapshotObject.ended because the content starts at the beginning again // after being restored. } else if (snapshotObject.ended) { // For postrolls, seek to the player's current duration. // It could be different from the snapshot's currentTime due to // inaccuracy in HLS. player.currentTime(player.duration()); } else { // Prerolls and midrolls, just seek to the player time before the ad. player.currentTime(snapshotObject.currentTime); const playPromise = player.play(); if (playPromise && playPromise.catch) { playPromise.catch((error) => { videojs.log.warn('Play promise rejected in snapshot resume', error); }); } } // if we added autoplay to force content loading on iOS, remove it now // that it has served its purpose if (player.ads.shouldRemoveAutoplay_) { player.autoplay(false); player.ads.shouldRemoveAutoplay_ = false; } }; // Determine if the video element has loaded enough of the snapshot source // to be ready to apply the rest of the state. // This only happens if the content video element was reused for ad playback. const tryToResume = function() { // tryToResume can either have been called through the `contentcanplay` // event or fired through setTimeout. // When tryToResume is called, we should make sure to clear out the other // way it could've been called by removing the listener and clearing out // the timeout. player.off('contentcanplay', tryToResume); if (tryToResumeTimeout_) { player.clearTimeout(tryToResumeTimeout_); } // Tech may have changed depending on the differences in sources of the // original video and that of the ad tech = player.el().querySelector('.vjs-tech'); if (tech.readyState > 1) { // some browsers and media aren't "seekable". // readyState greater than 1 allows for seeking without exceptions return resume(); } if (tech.seekable === undefined) { // if the tech doesn't expose the seekable time ranges, try to // resume playback immediately return resume(); } if (tech.seekable.length > 0) { // if some period of the video is seekable, resume playback return resume(); } // delay a bit and then check again unless we're out of attempts if (attempts--) { player.setTimeout(tryToResume, 50); } else { try { resume(); } catch (e) { videojs.log.warn('Failed to resume the content after an advertisement', e); } } }; if ('style' in snapshotObject) { // overwrite all css style properties to restore state precisely tech.setAttribute('style', snapshotObject.style || ''); } // Determine whether the player needs to be restored to its state // before ad playback began. With a custom ad display or burned-in // ads, the content player state hasn't been modified and so no // restoration is required if (player.ads.videoElementRecycled()) { // Snapshot restore is done, so now we're really finished. player.one('resumeended', () => { delete player.ads.snapshot; callback(); }); // on ios7, fiddling with textTracks too early will cause safari to crash player.one('contentloadedmetadata', restoreTracks); // adding autoplay guarantees that Safari will load the content so we can // seek back to the correct time after ads if (videojs.browser.IS_IOS && !player.autoplay()) { player.autoplay(true); // if we get here, the player was not originally configured to autoplay, // so we should remove it after it has served its purpose player.ads.shouldRemoveAutoplay_ = true; } // if the src changed for ad playback, reset it player.src(snapshotObject.sources); // and then resume from the snapshots time once the original src has loaded // in some browsers (firefox) `canplay` may not fire correctly. // Reace the `canplay` event with a timeout. player.one('contentcanplay', tryToResume); tryToResumeTimeout_ = player.setTimeout(tryToResume, 2000); } else { // if we didn't change the src, just restore the tracks restoreTracks(); // we don't need to check snapshotObject.ended here because the content video // element wasn't recycled if (!player.ended()) { // the src didn't change and this wasn't a postroll // just resume playback at the current time. const playPromise = player.play(); if (playPromise && playPromise.catch) { playPromise.catch((error) => { videojs.log.warn('Play promise rejected in snapshot restore', error); }); } } // snapshot restore is complete delete player.ads.snapshot; callback(); } }