PageSourceSearch

https://www.revolt.tv/scripts/revolt-player.js

js revolt.tv collected 2026-09-24 08:45:00 UTC 21,624 bytes, 455 lines download raw bytes

1/*
2 * REVOLT.TV Floating Video Player
3 * This script is used to embed the revolt.tv promo video player on a website.
4 *
5 * COPYRIGHT (C) 2024 YOUR NAME. ALL RIGHTS RESERVED.
6 */
7
8/* eslint-disable no-undef */
9
10import { getMetadata } from './lib-franklin.js';
11import { applyPrivacyToAdTag, resolveAdMode } from './privacy.js';
12import { suppressedAdvertising } from './jw-ads.js';
13
14const REVOLT_PLAYER_CONFIG = window.revolt_player_config;
15const PLAYER_ID = REVOLT_PLAYER_CONFIG[0]?.player_id || '14AVIIqU';
16const PLAYLIST_ID = REVOLT_PLAYER_CONFIG[0]?.playlist_id || 'gJDQAhak';
17const LABEL_ADVERTISEMENT = REVOLT_PLAYER_CONFIG[0]?.advertisement_label || 'Advertisement';
18const LABEL_HEADING = REVOLT_PLAYER_CONFIG[0]?.heading_label || 'Revolt - New Episodes';
19const LABEL_LINK_PREFIX = REVOLT_PLAYER_CONFIG[0]?.link_prefix_label || 'Watch:';
20const PLAYER_DELAY = Number(REVOLT_PLAYER_CONFIG[0]?.player_delay) || 3000;
21const SHOW_PLAYER_ARROW = REVOLT_PLAYER_CONFIG[0]?.playlist_cta_arrow
22  ? JSON.parse(REVOLT_PLAYER_CONFIG[0]?.playlist_cta_arrow)
23  : true;
24const SHOW_PLAYER_TEXT = REVOLT_PLAYER_CONFIG[0]?.playlist_title
25  ? JSON.parse(REVOLT_PLAYER_CONFIG[0]?.playlist_title)
26  : true;
27const SHOW_PLAYER_LINK_AREA = REVOLT_PLAYER_CONFIG[0]?.playlist_title_area
28  ? JSON.parse(REVOLT_PLAYER_CONFIG[0]?.playlist_title_area)
29  : true;
30const PLAYER_TITLE = REVOLT_PLAYER_CONFIG[0]?.playlist_title_key || 'seriesTitle';
31const PLAYER_TITLE_URL = REVOLT_PLAYER_CONFIG[0]?.playlist_title_url_key;
32const TITLE_LINE_LEN_DESKTOP = REVOLT_PLAYER_CONFIG[0]?.playlist_title_line_length_desktop || 2;
33const TITLE_LINE_LEN_MOBILE = REVOLT_PLAYER_CONFIG[0]?.playlist_title_line_length_mobile || 2;
34const PLAYLIST_MOBILE_CTA = REVOLT_PLAYER_CONFIG[0]?.playlist_mobile_cta
35  ? JSON.parse(REVOLT_PLAYER_CONFIG[0]?.playlist_mobile_cta)
36  : true;
37const MOBILE_CTA_LABEL = REVOLT_PLAYER_CONFIG[0]?.playlist_mobile_cta_label;
38const MOBILE_CTA_TARGET = REVOLT_PLAYER_CONFIG[0]?.playlist_mobile_cta_target;
39
40// Load stylesheet from /styles/revolt-player.css
41const styleSheet = document.createElement('link');
42styleSheet.rel = 'stylesheet';
43styleSheet.href = '/styles/revolt-player.css';
44document.head.appendChild(styleSheet);
45
46// Load player script
47
48const jwplayerScript = document.createElement('script');
49jwplayerScript.src = `https://cdn.jwplayer.com/libraries/${PLAYER_ID}.js`;
50jwplayerScript.async = true;
51document.head.appendChild(jwplayerScript);
52
53async function embedRevoltPlayer() {
54  // Player wrapper div
55  const containerDiv = document.createElement('div');
56  containerDiv.classList.add('revolt-player-container');
57
58  // Heading div
59  const headingDiv = document.createElement('div');
60  headingDiv.classList.add('revolt-player-heading');
61  const headingLabelDiv = document.createElement('div');
62  headingLabelDiv.classList.add('revolt-player-heading-label');
63  headingLabelDiv.innerHTML = LABEL_HEADING;
64  headingDiv.appendChild(headingLabelDiv);
65
66  // Mobile only link div
67  const mobileLinkDiv = document.createElement('div');
68  mobileLinkDiv.classList.add('revolt-player-mobile-link');
69  const mobileLinkElement = document.createElement('a');
70  mobileLinkElement.href = 'https://www.revolt.tv'; // Default url used before video starts
71  mobileLinkElement.textContent = `${LABEL_LINK_PREFIX} Revolt.tv`;
72  mobileLinkElement.style.webkitLineClamp = TITLE_LINE_LEN_MOBILE;
73  mobileLinkDiv.appendChild(mobileLinkElement);
74  const watchNowButton = document.createElement('a');
75  if (PLAYLIST_MOBILE_CTA) {
76    watchNowButton.classList.add('watch-now-btn');
77    watchNowButton.href = MOBILE_CTA_TARGET || 'https://www.revolt.tv'; // Default url used before video starts
78    watchNowButton.innerHTML = MOBILE_CTA_LABEL || 'Watch Now';
79    mobileLinkDiv.appendChild(watchNowButton);
80  }
81  headingDiv.appendChild(mobileLinkDiv);
82
83  const closeIcon = document.createElement('i');
84  closeIcon.classList.add('revolt-player-close');
85  closeIcon.style.display = 'none';
86  headingDiv.appendChild(closeIcon);
87  const handleClose = () => {
88    containerDiv.remove();
89  };
90
91  containerDiv.appendChild(headingDiv);
92
93  const playerDiv = document.createElement('div');
94  playerDiv.id = 'revolt-player';
95  containerDiv.appendChild(playerDiv);
96
97  const linkDiv = document.createElement('div');
98  linkDiv.classList.add(SHOW_PLAYER_LINK_AREA ? 'revolt-player-link' : 'revolt-player-link-hide');
99
100  const linkElement = document.createElement('a');
101  if (SHOW_PLAYER_TEXT) {
102    linkElement.href = 'https://www.revolt.tv'; // Default url used before video starts
103    linkElement.textContent = `${LABEL_LINK_PREFIX} Revolt.tv`;
104  }
105  linkElement.style.webkitLineClamp = TITLE_LINE_LEN_DESKTOP;
106  linkDiv.appendChild(linkElement);
107
108  const nextImageLinkElement = document.createElement('a');
109  nextImageLinkElement.href = 'https://www.revolt.tv'; // Default url used before video starts
110  nextImageLinkElement.classList.add(SHOW_PLAYER_ARROW ? 'revolt-player-next' : 'revolt-player-next-hide');
111  const nextImage = document.createElement('img');
112  nextImage.src = '/icons/revolt-player-next.png';
113  nextImage.alt = 'Watch Episode';
114  nextImageLinkElement.appendChild(nextImage);
115  linkDiv.appendChild(nextImageLinkElement);
116
117  const learnMoreButton = document.createElement('a');
118  learnMoreButton.classList.add('learn-more-btn');
119  learnMoreButton.innerHTML = 'Learn More';
120
121  const handleAdRedirection = (event, adClickURL) => {
122    window.open(adClickURL, '_blank');
123  };
124
125  containerDiv.appendChild(linkDiv);
126
127  document.body.appendChild(containerDiv);
128
129  // Fetch safetags from the current page. Kept as RAW members — every value
130  // that goes into cust_params is encoded once, at assembly, by encodeNested().
131  //
132  // Resolves TRUE when the brand-safety list was obtained and FALSE when it
133  // never arrived. The caller has to be able to tell "this page excludes no
134  // advertisers" from "we could not find out", because only the first is safe
135  // to serve an ad against: both used to collapse to an empty `safetags`, and
136  // stripEmptyParams() then dropped safetag_exclude_list from the tag
137  // altogether, so a brand-safety outage served a completely unfiltered VAST
138  // request. An empty Exclude IS a known answer — `Exclude: []` resolves true.
139  let safetags = [];
140  const maxRetries = 10;
141  const retryInterval = 250; // 500ms
142  function getSafetags() {
143    return new Promise((resolve) => {
144      let retryCount = 0;
145
146      function checkSafetags() {
147        if (window.safetag_lists && window.safetag_lists.Exclude) {
148          if (window.safetag_lists.Exclude.length > 0) {
149            safetags = window.safetag_lists.Exclude;
150          }
151          // eslint-disable-next-line no-console
152          console.log('player safetags', safetags);
153
154          resolve(true);
155        } else if (retryCount < maxRetries) {
156          retryCount += 1;
157          setTimeout(checkSafetags, retryInterval);
158        } else {
159          // eslint-disable-next-line no-console
160          console.error('revolt-player: brand-safety list unavailable after maximum retries — suppressing the ad schedule', window.location.pathname);
161          resolve(false);
162        }
163      }
164
165      checkSafetags();
166    });
167  }
168
169  const playerDelay = typeof jwplayer === 'undefined' ? 1000 : 0;
170  setTimeout(async () => {
171    // Player library 14AVIIqU carries a cloud-hosted ad schedule (8bHeWxdZ)
172    // which serves an ad — measured, including an impression — when no
173    // `advertising` key is passed at all: JW reads the missing key as "use my
174    // cloud schedule". That cloud ad never appears in
175    // getConfig().advertising.schedule, so it cannot be stamped, which is why
176    // this player previously had to suppress opted-out visitors outright
177    // (cloudAdvertising({ stampable: false })) — correct on compliance, but it
178    // cost every eligible opt-out impression. It now passes its OWN schedule,
179    // built and stamped below: an explicit schedule drops the cloud
180    // adscheduleid (measured on 14AVIIqU), so the tags that go out are ours
181    // and an eligible US opt-out visitor can be served a non-personalized ad
182    // instead of nothing. (WEB-606)
183    //
184    // Both are STARTED here so they overlap, and the ad mode is resolved FIRST
185    // so the safetag poll is only ever waited on when it will be used.
186    // getSafetags() polls for up to maxRetries x retryInterval (~2.5s) when
187    // window.safetag_lists never appears, and a visitor who is getting an empty
188    // schedule anyway must not pay that wait before the CONTENT player is set
189    // up. On the serving path the wait is required: building a tag without the
190    // list sends an empty safetag_exclude_list, i.e. an unfiltered ad call on a
191    // page the brand-safety scan may have excluded advertisers from.
192    const adModePromise = resolveAdMode();
193    const safetagsPromise = getSafetags();
194
195    const { load, nonPersonalized } = await adModePromise;
196    // Only the serving path needs the list, so only it pays the poll. A
197    // suppressed visitor is already getting an empty schedule.
198    const safetagsKnown = load ? await safetagsPromise : false;
199    // Fail CLOSED on a brand-safety miss: without the exclusion list we cannot
200    // assert the request is filtered, and awaiting the poll would otherwise
201    // only delay an unfiltered ad call rather than prevent one.
202    //
203    // This deliberately diverges from the display stack, which fails OPEN on
204    // the same signal (see the documented decision in scripts.js:821), so
205    // during a brand-safety outage this page serves unfiltered display ads
206    // while video stays dark. Raised on PR #1885 and settled there: video
207    // stays fail-closed, because withholding a video impression is cheaper
208    // than serving one the brand-safety scan never cleared. Deliberate, not an
209    // oversight — do not "fix" the inconsistency by dropping `&& safetagsKnown`
210    // here. Closing it the other way (display fail-closed too) is the better
211    // change and needs its own ticket.
212    const serveAds = load && safetagsKnown;
213
214    // Retrieve metadata values once
215    const metadata = {
216      internalTags: (getMetadata('internal-tags') || '')
217        .split(',')
218        .map((tag) => tag.trim())
219        .filter((tag) => tag !== ''),
220      post_id: getMetadata('post_id'),
221      // Normalised the same way as the display-ad targeting in scripts.js so
222      // both ad calls send an identical `category` value for GAM to key line
223      // items on. The authored tag in revolt-player-config already carries a
224      // {category} placeholder that nothing was substituting, so the literal
225      // token would otherwise go out as the value.
226      categories: (getMetadata('categories') || '')
227        .split(',')
228        .map((category) => category.trim().toLowerCase().replace(/\s+/g, '-'))
229        .filter((category) => category !== ''),
230    };
231
232    // cust_params is itself a percent-encoded query string NESTED inside the tag
233    // URL, so GAM decodes the outer layer before parsing the inner key/value
234    // pairs. A value has to survive BOTH decodes: one encodeURIComponent leaves
235    // a reserved separator intact after the outer decode, which terminates the
236    // pair and invents a key — `AT&T` in the exclude list would arrive as
237    // `safetag_exclude_list=AT` plus a stray `T`, silently dropping a
238    // brand-safety exclusion. Encode twice so the inner layer sees `AT%26T`.
239    const encodeNested = (value) => encodeURIComponent(encodeURIComponent(value));
240    // Multi-value keys: GAM reads a LITERAL comma inside cust_params as its
241    // value separator, so each member is encoded on its own and joined with a
242    // bare comma. Encoding the joined string would send `news%2Crap-up` — one
243    // value — where the display targeting in scripts.js sends two.
244    const encodeNestedList = (values) => values.map(encodeNested).join(',');
245
246    const params = {
247      safetags: safetags.length ? encodeNestedList(safetags) : '',
248      internal_tag: metadata.internalTags.length ? encodeNestedList(metadata.internalTags) : '',
249      post_id: metadata.post_id ? encodeNested(metadata.post_id) : '',
250      category: metadata.categories.length ? encodeNestedList(metadata.categories) : '',
251    };
252
253    let preRolladTagUrl = REVOLT_PLAYER_CONFIG[0]?.preroll_ad_tag_url || 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21787171759/revolt.tv/site/preroll_float_640x480v&description_url={currentPage}&sz=640x480&gdfp_req=1&output=xml_vast2&unviewed_position_start=1&env=vp&cust_params=safetag_exclude_list%3D{safetags}%26platform%3Dfloating%26internal_tag%3D{internal_tag}%26post_id%3D{post_id}%26category%3D{category}';
254
255    let midRolladTagUrl = REVOLT_PLAYER_CONFIG[0]?.midroll_ad_tag_url || 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21787171759/revolt.tv/site/midroll_float_640x480v&description_url={currentPage}&sz=640x480&gdfp_req=1&output=xml_vast2&unviewed_position_start=1&env=vp&cust_params=safetag_exclude_list%3D{safetags}%26platform%3Dfloating%26internal_tag%3D{internal_tag}%26post_id%3D{post_id}%26category%3D{category}';
256
257    preRolladTagUrl = preRolladTagUrl.replace('{currentPage}', encodeURIComponent(window.location.href));
258    midRolladTagUrl = midRolladTagUrl.replace('{currentPage}', encodeURIComponent(window.location.href));
259
260    // Replace placeholders dynamically
261    Object.keys(params).forEach((key) => {
262      preRolladTagUrl = preRolladTagUrl.replace(`{${key}}`, params[key]);
263      midRolladTagUrl = midRolladTagUrl.replace(`{${key}}`, params[key]);
264    });
265
266    const rngCorrelator = Math.floor(Math.random() * 9999999).toString();
267    preRolladTagUrl = `${preRolladTagUrl}&correlator=${rngCorrelator}`;
268    midRolladTagUrl = `${midRolladTagUrl}&correlator=${rngCorrelator}`;
269
270    // Drop any cust_params key whose value came out empty.
271    //
272    // Scoped to the cust_params VALUE, not the whole URL: description_url
273    // carries the percent-encoded page URL, so a visitor arriving with an
274    // empty query param of their own (`?utm_source=x&utm_campaign=`) has a
275    // `%26utm_campaign%3D` sitting immediately before the tag's raw
276    // `&cust_params`. Run over the whole URL, this silently deleted that param
277    // from the page URL GAM reads for contextual targeting.
278    //
279    // Within the value, keys are separated by %26 and terminated by end of
280    // value. `(^|%26)` covers the FIRST key too — safetag_exclude_list leads the
281    // value, so an empty exclude list (or a brand-safety timeout) otherwise sent
282    // `safetag_exclude_list=` despite this "drop any empty key" pass. Removing a
283    // leading key can leave the value starting with %26, which is trimmed after.
284    // The lookahead (rather than a greedy `[^=]+`) is what keeps an empty LAST
285    // key from swallowing every key before it — a page with no categories used
286    // to lose platform, internal_tag and post_id too.
287    // See revolt-player-ad-tag.test.mjs.
288    const stripEmptyParams = (tag) => tag
289      .replace(/([?&]cust_params=)([^&]*)/, (_, prefix, value) => prefix + value
290        .replace(/(^|%26)\w+%3D(?=%26|$)/g, '')
291        .replace(/^%26/, ''))
292      .replace(/(&|\?)$/, '');
293
294    preRolladTagUrl = stripEmptyParams(preRolladTagUrl);
295    midRolladTagUrl = stripEmptyParams(midRolladTagUrl);
296
297    // Stamp the visitor's US Privacy opt-out state onto the GAM tags
298    // (covers tags supplied via AEM revolt-player-config as well), using the
299    // same resolved opt-out state the gate above used rather than a second,
300    // possibly inconsistent read
301    preRolladTagUrl = applyPrivacyToAdTag(preRolladTagUrl, nonPersonalized);
302    midRolladTagUrl = applyPrivacyToAdTag(midRolladTagUrl, nonPersonalized);
303
304    const playerSetup = {
305      playlist: `https://cdn.jwplayer.com/v2/playlists/${PLAYLIST_ID}?format=json`,
306      autostart: true,
307      mute: true,
308    };
309
310    // An eligible US opt-out visitor is served this schedule too — every tag
311    // in it was stamped npa=1/rdp=1/us_privacy=1YYY by applyPrivacyToAdTag()
312    // above, using the same resolved state this gate reads. Anyone who must not
313    // be served — an opt-in region with no consent, an unknown region,
314    // OneTrust never resolving, or a brand-safety list we never obtained —
315    // gets an EXPLICIT empty schedule rather than no `advertising` key,
316    // because omitting it hands them the cloud schedule.
317    playerSetup.advertising = serveAds
318      ? {
319        client: 'vast',
320        schedule: [
321          { offset: 'pre', tag: preRolladTagUrl },
322          { offset: 30, tag: midRolladTagUrl },
323          { offset: 60, tag: midRolladTagUrl },
324          { offset: 90, tag: midRolladTagUrl },
325          { offset: 120, tag: midRolladTagUrl },
326          { offset: 150, tag: midRolladTagUrl },
327          { offset: 180, tag: midRolladTagUrl },
328          { offset: 210, tag: midRolladTagUrl },
329          { offset: 240, tag: midRolladTagUrl },
330          { offset: 270, tag: midRolladTagUrl },
331        ],
332      }
333      : suppressedAdvertising('vast');
334
335    const player = jwplayer('revolt-player').setup(playerSetup);
336
337    player.on('adTime', () => {
338      closeIcon.style.display = 'none';
339      closeIcon.removeEventListener('click', handleClose);
340    });
341
342    player.on('play', () => {
343      if (!player.getState().includes('ad')) {
344        closeIcon.style.display = 'block'; // Show only if not an ad
345        closeIcon.addEventListener('click', handleClose);
346        // Set link to show url and video title
347        const playlistItem = jwplayer('revolt-player').getPlaylistItem();
348        const playerTitle = playlistItem[PLAYER_TITLE] || playlistItem.seriesTitle;
349        const playerTitleURL = playlistItem[PLAYER_TITLE_URL];
350
351        const seriesUrl = playerTitle.replace(/: /g, '-').replace(/[^a-zA-Z0-9- ]+/g, '').replace(/ /g, '-').toLowerCase();
352        const showUrl = `https://www.revolt.tv/show/${seriesUrl}?ref=sticky_player`;
353        if (SHOW_PLAYER_TEXT) {
354          linkElement.href = playerTitleURL || showUrl;
355          linkElement.textContent = `${LABEL_LINK_PREFIX} ${playerTitle}`;
356        }
357
358        mobileLinkElement.textContent = playerTitle;
359        mobileLinkElement.href = playerTitleURL || showUrl;
360        watchNowButton.href = playerTitleURL || showUrl;
361        if (SHOW_PLAYER_ARROW) {
362          nextImageLinkElement.href = playerTitleURL || showUrl;
363        }
364      }
365    });
366
367    player.on('adStarted', () => {
368      closeIcon.style.display = 'none';
369      closeIcon.removeEventListener('click', handleClose);
370    });
371
372    // Advertisment Start Event - Update title, link
373    jwplayer('revolt-player').on('adImpression', (event) => {
374      const adClickURL = event.clickThroughUrl;
375
376      document.querySelector('.revolt-player-heading').style.display = 'none';
377      if (!adClickURL) {
378        const container = document.querySelector('.revolt-player-container');
379        Object.assign(container.style, {
380          background: 'black',
381          gridTemplateColumns: '50%',
382          justifyContent: 'center',
383        });
384      }
385      watchNowButton.style.display = 'none';
386      learnMoreButton.addEventListener('click', (clickEvent) => {
387        if (adClickURL) {
388          handleAdRedirection(clickEvent, adClickURL);
389          mobileLinkDiv.appendChild(learnMoreButton);
390        }
391      });
392      closeIcon.style.display = 'none';
393      closeIcon.removeEventListener('click', handleClose);
394      mobileLinkElement.style.display = 'none';
395
396      // Change title to Advertisement
397      headingLabelDiv.innerHTML = LABEL_ADVERTISEMENT;
398
399      // Hide links
400      if (!linkDiv.classList.contains('hide-link')) {
401        linkDiv.classList.add('hide-link');
402      }
403    });
404
405    // Advertisement End Event - Update title, link
406    jwplayer('revolt-player').on('adComplete adSkipped', () => {
407      document.querySelector('.revolt-player-heading').style.display = 'grid';
408      document.querySelector('.revolt-player-container').style.gridTemplateColumns = '50% 50%';
409
410      watchNowButton.style.display = 'block';
411
412      learnMoreButton.removeEventListener('click', () => handleAdRedirection());
413      closeIcon.style.display = 'block';
414      closeIcon.addEventListener('click', handleClose);
415
416      mobileLinkElement.style.display = '-webkit-box';
417
418      learnMoreButton.remove();
419
420      // Reset title
421      headingLabelDiv.innerHTML = LABEL_HEADING;
422
423      // Show links
424      if (linkDiv.classList.contains('hide-link')) {
425        linkDiv.classList.remove('hide-link');
426      }
427
428      // Set link to show url and video title
429      const playlistItem = jwplayer('revolt-player').getPlaylistItem();
430      const playerTitle = playlistItem[PLAYER_TITLE] || playlistItem.seriesTitle;
431      const playerTitleURL = playlistItem[PLAYER_TITLE_URL];
432      const seriesUrl = playerTitle.replace(/: /g, '-').replace(/[^a-zA-Z0-9- ]+/g, '').replace(/ /g, '-').toLowerCase();
433      const showUrl = `https://www.revolt.tv/show/${seriesUrl}?ref=sticky_player`;
434      if (SHOW_PLAYER_TEXT) {
435        linkElement.textContent = `${LABEL_LINK_PREFIX} ${playerTitle}`;
436        linkElement.href = playerTitleURL || showUrl;
437      }
438
439      mobileLinkElement.textContent = playerTitle;
440      mobileLinkElement.href = playerTitleURL || showUrl;
441      watchNowButton.href = playerTitleURL || showUrl;
442      if (SHOW_PLAYER_ARROW) {
443        nextImageLinkElement.href = playerTitleURL || showUrl;
444      }
445    });
446  }, playerDelay);
447}
448
449// Call revolt floating video player
450const additionalPlayerDelay = window.revoltAddDelay || 0;
451const totalPlayerDelay = PLAYER_DELAY + additionalPlayerDelay;
452
453setTimeout(() => {
454  embedRevoltPlayer();
455}, totalPlayerDelay);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.