PageSourceSearch

https://www.nbcwashington.com/wp-content/themes/nbc-station/static…/videoHeartbeat/JWHeartbeat.js?ver=1.2.2.Bo

js nbcwashington.com collected 2026-09-24 08:28:27 UTC 23,004 bytes, 702 lines download raw bytes

1/* eslint-disable id-length */
2/* eslint-disable camelcase */
3/* eslint-disable no-prototype-builtins */
4/* eslint-disable no-unused-vars */
5/* eslint-disable no-underscore-dangle */
6/* eslint-disable no-console */
7/* eslint-disable func-names */
8/* global ADB */
9
10/*
11 * Please Note: This plugin implementation is a Proof of Concept only provided
12 * to show the possibilities of the JW Player and should not be taken as an
13 * offer to create, edit or maintain custom integration or development.
14 */
15
16/* This function is automatcally instantiated when referenced in the
17 * JWPlayer Config like so:
18 *            plugins: {
19 *               './scripts/JWHeartbeat.js' : {
20 *                   adobeTrackingDomain: '<TRACKING SERVER DOMAIN>',
21 *                   channelName: '<Channel Name>',
22 *                   channel: <type of channel>,
23 *                   pageName: <source page name>,
24 *                   debug: <true or false>
25 *               }
26 *            }
27 */
28(function () {
29  let gDebugOn = false;
30
31  // Simple debug function
32  //
33  function log(str, obj = null) {
34    if (gDebugOn) {
35      const msg = `[jwhb]: ${str}`;
36      if (null !== obj) console.log(msg, obj);
37      else console.log(msg);
38    }
39  }
40
41  function testTimeStamps(offset) {
42    const currentTimestamp = Date.now();
43    const futureTimestamp = currentTimestamp + offset;
44
45    const futureDate = new Date(futureTimestamp);
46
47    return futureDate.toUTCString();
48  }
49
50  // Setup default config values
51  //
52
53  const id = 'JWHeartbeat'; // Id name must be same name as .js file
54  const _defaults = {
55    channelName: 'JW Heartbeat Plugin',
56    adobeTrackingDomain: '',
57    debug: false,
58    pageName: 'pagename Not Set',
59    channel: 'Video',
60  };
61
62  // This is the main plugin function.  It gets called by the player when
63  // it is registered below.
64  //
65  const plugin = function (player, config) {
66    // Local variables
67    //
68    const { _ } = player;
69    const _config = _.extend({}, _defaults, config);
70    const _player = player;
71    let _state;
72    const _trackingServer = _config.adobeTrackingDomain;
73    const _adobeChannel = _config.channelName;
74
75    gDebugOn = _config.debug;
76
77    log('plugin Initializing: config', _config);
78
79    if ('' === _trackingServer) {
80      console.log('[jwhb]: Tracking server not initialized.');
81      return;
82    }
83
84    // Heartbeat initialization, 's' is defined in AppMeasurement.js
85    //
86    log('Setting AppMeasurement');
87    s.pageName = _config.pageName;
88    s.channel = _config.channel;
89
90    // Variable initialization.  Some events produce data that is needed for
91    // subsequent events, so we need to save the data
92    //
93    let _Heartbeat = {};
94    let _MediaInfo = {};
95    let _CustomParams = {};
96    let _AdPosition = 0;
97    let _ChapterInfo = {};
98    const _ChapterMeta = {};
99    let mediaDelegate = {};
100    let fastSchedule = [];
101
102    // Saves last known playback to determine actual playhead position based
103    // on system time and on('time') event
104    //
105    let _LastKnownPlaybackPosition = 0;
106    let _LastSystemClock = 0;
107    let _StartUpTime = 0;
108    let _Bitrate = 50000;
109
110    // Flags
111    //
112    let _sessionStarted = false;
113    let _PlaySent = false;
114    let _AdsPlaying = false;
115    let _IsPlaying = false;
116    let _IsBuffering = false;
117    let _IsLive = false;
118    let _offsetTime = 0;
119    let _offsetTimeIsSet = false;
120    let _firstChapterFired = false;
121
122    // Media Heartbeat variables
123    //
124    const _MediaHeartbeat = ADB.va.MediaHeartbeat;
125    const _MediaHeartbeatConfig = ADB.va.MediaHeartbeatConfig;
126    const _MediaHeartbeatDelegate = ADB.va.MediaHeartbeatDelegate;
127
128    function setChapterInfo(data, offset) {
129      return _MediaHeartbeat.createChapterObject(
130        data.title,
131        data.chapter,
132        data.duration,
133        offset
134      );
135    }
136
137    // Code to initialize the heartbeat instance
138    //
139    function setupHeartbeat() {
140      // log('setupHeartbeat');
141
142      // Media Heartbeat initialization
143      //
144      const mediaConfig = new _MediaHeartbeatConfig();
145      mediaConfig.trackingServer = _trackingServer;
146
147      const configuration = _player.getConfig();
148      mediaConfig.playerName = 'jw player'; // the JW Player ID
149      mediaConfig.channel = _adobeChannel;
150      mediaConfig.debugLogging = gDebugOn;
151      mediaConfig.appVersion = _player.version; // The JW Player version
152      mediaConfig.ssl = true;
153      mediaConfig.ovp = 'jwplayer';
154
155      // Media Heartbeat Delegate
156      //
157      mediaDelegate = new _MediaHeartbeatDelegate();
158
159      // Create the mediaHeartbeat instance s is defined elsewhere.
160      //
161      // log('Creating the MediaHeartbeat object.', mediaConfig);
162      _Heartbeat = new _MediaHeartbeat(mediaDelegate, mediaConfig, s);
163
164      // Set mediaDelegate Current Playbacktime
165      //
166      mediaDelegate.getCurrentPlaybackTime = function () {
167        // log(`mediaDelegate.getCurrentPlaybackTime: position ${pos} state: ${player.getState()} Ads Playing: ${_AdsPlaying}`);
168        return _LastKnownPlaybackPosition - _offsetTime;
169      };
170      mediaDelegate.getQoSObject = function (data) {
171        // log('_getQosObject: Enter');
172        let droppedFrames = 0;
173
174        const elem = player.getMediaElement();
175
176        if (elem && elem.getVideoPlaybackQuality()) {
177          droppedFrames = elem.getVideoPlaybackQuality().droppedVideoFrames;
178        }
179
180        const quality = player.getVisualQuality();
181
182        if (quality) {
183          _Bitrate = quality.level.bitrate;
184        }
185
186        const qos = _MediaHeartbeat.createQoSObject(_Bitrate, _StartUpTime, 29, droppedFrames);
187        return qos;
188      };
189    }
190    // Create an Error string of the player error
191    //
192    function createErrorString(data) {
193      let cache = [];
194      const str = JSON.stringify(data, (key, value) => {
195        if ('object' === typeof value && null !== value) {
196          if (- 1 !== cache.indexOf(value)) {
197            // Duplicate reference found
198            try {
199              // If this value does not reference a parent it can be deduped
200              return JSON.parse(JSON.stringify(value));
201            } catch (error) {
202              // discard key if value cannot be deduped
203              return '';
204            }
205          }
206          // Store value in our collection
207          cache.push(value);
208        }
209        return value;
210      });
211      cache = null; // Enable garbage collection
212      return str;
213    }
214
215    // Advertising Events
216    //
217    function adStartBreak(name, pos, start) {
218      // Replace <ADBREAK_NAME> with the AdBreak name.
219      // Replace <POSITION> with a valid position value.
220      // Replace <START_TIME> with the AdBreak start time.
221      // log('adStartBreak: Send #trackEvent:AdStartBreak');
222      const adBreakInfo = _MediaHeartbeat.createAdBreakObject(name, pos, start);
223      _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdBreakStart, adBreakInfo);
224      _AdsPlaying = true;
225      _AdPosition = 0;
226      // _AdPosition += 1; // Just increase the adPosition value
227      // log(`adStartBreak: Ad in position ${_AdPosition} starting`);
228    }
229    function resetSession() {
230      // log('Send #trackSessionEnd');
231      _Heartbeat.trackSessionEnd();
232      _sessionStarted = false; // Will need to reinitialize for next video
233      _PlaySent = false; // Reset.
234      _IsPlaying = false;
235      _IsBuffering = false;
236    }
237
238    function setupListeners() {
239      // log('setupListeners');
240
241      // When playlistItem fires, get the information and create a MediaInfo object
242      // to be used to start the session
243      //
244      player.on('playlistItem', (info) => {
245        // log('playlistItem', info.item);
246        if (_sessionStarted) {
247          log('resetSession');
248          resetSession();
249        }
250
251        const configuration = player.getConfig();
252
253        let title = 'unknown';
254        let mediaid = 'unknown';
255
256        if (info.item.hasOwnProperty('videoTitle')) {
257          title = info.item.videoTitle;
258        }
259
260        let mpxTitle = title;
261
262        if (info.item.hasOwnProperty('mpxTitle')) {
263          mpxTitle = info.item.mpxTitle;
264        }
265
266        if (info.item.hasOwnProperty('mediaid')) {
267          mediaid = info.item.mediaid;
268        } else if (info.item.hasOwnProperty('file')) {
269          mediaid = info.item.file;
270        }
271
272        if (info.item.hasOwnProperty('fastSchedule')) {
273          fastSchedule = info.item.fastSchedule;
274
275          fastSchedule.forEach((obj) => {
276            const show = obj;
277            show.started = false;
278          });
279
280          if (0 < fastSchedule.length) {
281            _ChapterMeta.chapter_id = fastSchedule[0].id;
282            _ChapterMeta.chapter_sid = fastSchedule[0].seriesId;
283          }
284        }
285
286        if ('1' === info.item.isLivestream) {
287          _IsLive = true;
288        }
289
290        // Set media info information
291        // MediaHeartbeat.createMediaObject(<VIDEO_NAME>, <VIDEO_ID>, <VIDEO_LENGTH>,MediaHeartbeat.StreamType.VOD);
292        //
293        _MediaInfo = _MediaHeartbeat.createMediaObject(
294          mpxTitle,
295          String(mediaid),
296          info.item.duration,
297          '1' === info.item.isLivestream ? _MediaHeartbeat.StreamType.LIVE : _MediaHeartbeat.StreamType.VOD,
298        );
299
300        // Set standard video metadata
301        //
302        let standardVideoMetadata = {};
303        standardVideoMetadata[_MediaHeartbeat.VideoMetadataKeys.ASSET_ID] = mediaid;
304        standardVideoMetadata = { ...standardVideoMetadata, ...info.item.meta };
305        _MediaInfo.setValue(_MediaHeartbeat.MediaObjectKey.StandardVideoMetadata, standardVideoMetadata);
306
307        // Note that JWP Custom Parameters are also available in the metadata object and
308        // can be sent to Adobe heartbeat. e.g. If not tracking custom parameters
309        // then send 'null' to trackSessionStart
310        //
311        _CustomParams = {};
312        // For example, if there is a JW Custom parameter with key 'import_guid'
313        //
314        // _CustomParams.guid = metadata.import_guid;
315
316        if (0 < fastSchedule.length) {
317          _ChapterInfo = setChapterInfo(fastSchedule[0], Math.round((Date.now() - Date.parse(fastSchedule[0].startTime)) / 60000));
318          window.addEventListener('beforeunload', () => {
319            _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterComplete, _ChapterInfo, _ChapterMeta);
320          });
321        } else {
322          _ChapterInfo = _MediaHeartbeat.createChapterObject(title, 1, info.item.duration, 0);
323        }
324
325        // Initialize play positions
326        //
327        _LastKnownPlaybackPosition = 0;
328        _LastSystemClock = 0;
329        _PlaySent = false;
330      });
331
332      // A video or ad is about to begin playing
333      //   TODO: Maybe this should be player.once?
334      //
335      player.on('beforePlay', (data) => {
336        // log('beforePlay event, position: ', player.getPosition());
337
338        // Initialize the Heartbeat for this session
339        // The JW event can get called multiple times with a position of 0,
340        // but should only initialize the heartbeat session once
341        //
342        if (0 === player.getPosition()) {
343          if (! _sessionStarted) {
344            // log('Send #trackSessionStart');
345            _sessionStarted = true;
346
347            // Start Session, This signals the user's intent to watch video
348            //
349            _Heartbeat.trackSessionStart(_MediaInfo, _CustomParams);
350          }
351        }
352        // If the current state is paused, then resend a play event
353        //
354        const state = player.getState();
355        if ('paused' === state) {
356          // log('beforePlay: Send #trackPlay()');
357          _Heartbeat.trackPlay();
358          _IsPlaying = true;
359        }
360      });
361
362      // FirstFrame of content has been played.
363      //
364      player.on('firstFrame', (data) => {
365        // log('firstFrame');
366        // If haven't sent the play event then send it.  Note that it's possible the
367        // play vent was sent when the AdStart event was signaled
368        //
369        if (_IsLive && 0 < player.getPosition() && ! _offsetTimeIsSet) {
370          _offsetTime = player.getPosition();
371          _offsetTimeIsSet = true;
372        }
373        if (! _PlaySent && _AdsPlaying) {
374          // log('firstFrame: Send #trackPlay');
375          _Heartbeat.trackPlay();
376          _PlaySent = true;
377        }
378
379        if (data.loadTime) {
380          _StartUpTime = data.loadTime;
381        }
382        _IsPlaying = true;
383
384        _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterStart, _ChapterInfo, 0 < fastSchedule.length ? _ChapterMeta : null);
385        _firstChapterFired = true;
386      });
387
388      // Save the current system clock, and current playback position being
389      // reported to calclulate the playback position at a later time
390      //
391      player.on('time', (data) => {
392        const d = new Date();
393        _LastSystemClock = d.getTime();
394        if (_IsLive) {
395          _LastKnownPlaybackPosition = player.getMediaElement().currentTime;
396        } else {
397          _LastKnownPlaybackPosition = data.position;
398        }
399        if (0 < fastSchedule.length) {
400          if (Date.parse(fastSchedule[0].endTime) < Date.now()) {
401            _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterComplete, _ChapterInfo, _ChapterMeta);
402            fastSchedule.shift();
403            _ChapterInfo = setChapterInfo(fastSchedule[0], 0);
404            _ChapterMeta.chapter_id = fastSchedule[0].id;
405            _ChapterMeta.chapter_sid = fastSchedule[0].seriesId;
406          }
407
408          if (! fastSchedule[0].started && Date.parse(fastSchedule[0].startTime) < Date.now() && Date.parse(fastSchedule[0].endTime) > Date.now() && _firstChapterFired) {
409            _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterStart, _ChapterInfo, _ChapterMeta);
410            fastSchedule[0].started = true;
411          }
412        }
413      });
414
415      // Seeking Events
416      //
417      player.on('seek', (data) => {
418        log('Send #trackEvent:SeekStart');
419        _Heartbeat.trackEvent(_MediaHeartbeat.Event.SeekStart);
420        _IsPlaying = false;
421      });
422
423      player.on('seeked', (data) => {
424        log('Send #trackEvent:SeekComplete');
425        _LastKnownPlaybackPosition = player.getPosition(); // Save last known position
426        _Heartbeat.trackEvent(_MediaHeartbeat.Event.SeekComplete);
427        _IsPlaying = true;
428      });
429
430      // User paused the video
431      //
432      player.on('pause', (data) => {
433        log('send #trackPause');
434        _Heartbeat.trackPause();
435        _IsPlaying = false;
436      });
437
438      // AD paused
439      //
440      player.on('adPause', (data) => {
441        log('send #trackAdPause');
442        _Heartbeat.trackPause();
443        _IsPlaying = false;
444      });
445
446      // If the play event is fired and ads were playing,
447      // then send the AdBreakComplete event.  There is
448      // no ad playback end event
449      //
450      player.on('play', (data) => {
451        if (_IsLive && 0 < player.getPosition() && ! _offsetTimeIsSet) {
452          _offsetTime = player.getPosition();
453          _offsetTimeIsSet = true;
454        }
455        _Heartbeat.trackPlay();
456        _IsPlaying = true;
457        if (_AdsPlaying) {
458          log('play: Send #trackEvent:AdBreakComplete');
459          _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdBreakComplete);
460          _AdsPlaying = false;
461        }
462      });
463
464      player.on('idle', () => {
465        log('idle');
466        if (_IsLive) {
467          _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterComplete);
468          _Heartbeat.trackComplete();
469          resetSession();
470        }
471      });
472
473      // Video (and ads) have completed
474      //
475      player.on('complete', (data) => {
476        log('complete');
477
478        // If an ad was playing (POST-ROLL), send the AdBreakComplete
479        //
480        if (_AdsPlaying) {
481          log('complete: (POST-ROLL) Send #AdBreakComplete');
482          _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdBreakComplete);
483          _AdsPlaying = false;
484        }
485        _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterComplete);
486        log('Send #trackComplete');
487        _Heartbeat.trackComplete();
488        resetSession();
489      });
490
491      // Only send the buffer event if the player is actually playing video
492      // We don't send one just because the player loaded and pre-buffered or seeked
493      //
494      player.on('buffer', (data) => {
495        log(`buffer: Player State ${player.getState()}`);
496        if (_IsPlaying) {
497          log('Send #trackEvent:BufferStart');
498          // _Heartbeat.trackEvent(_MediaHeartbeat.Event.BufferStart);
499          _IsBuffering = true;
500        }
501      });
502      player.on('bufferFull', (data) => {
503        log(`bufferFull: Player State ${player.getState()}`);
504        if (_IsBuffering) {
505          log('Send #trackEvent:BufferComplete');
506          // _Heartbeat.trackEvent(_MediaHeartbeat.Event.BufferComplete);
507          _IsBuffering = false;
508        }
509      });
510      //
511      // Error Handling
512      //
513      player.on('error', (data) => {
514        log('(error) Send #trackError');
515
516        //
517        // Send all the error data as a string
518        //
519        _Heartbeat.trackError(createErrorString(data));
520
521        // error means the player stopped so reset the session
522        resetSession();
523      });
524
525      // Handle a setup error.  Since the player hasn't even started to play, the
526      // code needs to send a session start, the error, then the session end
527      //
528      player.on('setupError', (data) => {
529        if (! _sessionStarted) {
530          const configuration = player.getConfig();
531          let title = 'unknown';
532          let mediaId = 'unknown';
533
534          // See if we can get the title and media ID out of the setup config
535          //
536          if (configuration.hasOwnProperty('setupConfig')) {
537            const { setupConfig } = configuration;
538            if (setupConfig.hasOwnProperty('playlist')) {
539              const { playlist } = setupConfig;
540              if (Array.isArray(playlist)) {
541                if (playlist[0].hasOwnProperty('title')) {
542                  title = playlist[0].title;
543                }
544                if (playlist[0].hasOwnProperty('mediaid')) {
545                  mediaId = playlist[0].mediaid;
546                }
547              }
548            }
549          }
550          // Create the info needed for the session start
551          //
552          const info = _MediaHeartbeat.createMediaObject(
553            title,
554            String(mediaId),
555            0,
556            _MediaHeartbeat.StreamType.VOD
557          );
558
559          // Set standard video metadata
560          //
561          const standardVideoMetadata = {};
562          standardVideoMetadata[_MediaHeartbeat.VideoMetadataKeys.ASSET_ID] = String(mediaId);
563          info.setValue(_MediaHeartbeat.MediaObjectKey.StandardVideoMetadata, standardVideoMetadata);
564          log('eventHandler: (setupError) Send #trackSessionStart');
565          _Heartbeat.trackSessionStart(info, {});
566        }
567        log('eventHandler: (setupError) Send #trackError');
568        _Heartbeat.trackError(createErrorString(data));
569
570        resetSession();
571      });
572
573      // Use the adImpression event to signal that an ad is being started
574      //
575      player.on('adImpression', (data) => {
576        log('adImpression');
577
578        const {
579          adtitle,
580          position,
581          id: adId,
582          duration,
583          tag,
584          client,
585          timeLoading,
586        } = data;
587
588        if (timeLoading) {
589          _StartUpTime = timeLoading;
590          _Heartbeat.trackEvent(_MediaHeartbeat.Event.BitrateChange, mediaDelegate.getQoSObject(data));
591        }
592
593        // Start the adBreak if needed
594        //
595        adStartBreak(adtitle, position, _LastKnownPlaybackPosition);
596
597        // Create an adObject and custom metadata to track the ad
598        //
599        const adObject = _MediaHeartbeat.createAdObject(adtitle, adId, _AdPosition, duration);
600        const adCustomMetadata = {
601          tag,
602          client,
603        };
604
605        // Send the AdStart event
606        //
607        log('adImpression: Send #trackEvent:AdStart');
608        _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdStart, adObject, adCustomMetadata);
609
610        // Note: The documentation says that after the first ad in a Pre-roll
611        // starts to play, the 'trackPlay' event should be sent.  But, it seems,
612        // in practice, this does not work correctly.  The adStart event that is sent
613        // above in the adImpression handler, automatically kicks off the trackPlay
614        // event.  Thus, just set the gPlaySent flag to true, so it is not sent again
615        //
616        if (! _PlaySent) {
617          _PlaySent = true;
618          log('adImpression: Sent #trackPlay');
619          _Heartbeat.trackPlay();
620        }
621        // Set the flag that an Ad is playing
622        //
623        _AdsPlaying = true;
624      });
625
626      // ad is being played.
627      // If haven't sent the trackPlay event, then send it
628      //
629      player.on('adPlay', (data) => {
630        log('adPlay');
631        if (! _PlaySent) {
632          log('adPlay: Send #trackPlay');
633          _Heartbeat.trackPlay();
634          _PlaySent = true;
635          _IsPlaying = true;
636        }
637
638        if (! _IsPlaying) {
639          _Heartbeat.trackPlay();
640          _IsPlaying = true;
641        }
642      });
643
644      player.on('adStarted', (data) => {
645        log('adStarted. do nothing.');
646      });
647
648      player.on('adSkipped', (data) => {
649        log('adSkipped: Send #trackEvent:AdSkip');
650        _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdSkip);
651
652        // Set the flag to false so the AdBreakComplete is not sent
653        // unless a new ad starts playing
654        _AdsPlaying = false;
655      });
656
657      player.on('adComplete', (data) => {
658        log('adComplete: Send #trackEvent:AdComplete');
659        _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdComplete);
660      });
661
662      player.on('adBreakEnd', (data) => {
663        log('eventHandler: adBreakEnd');
664
665        // If an ad was playing send the AdBreakComplete
666        //
667        if (_AdsPlaying) {
668          log('adBreakEnd: Send #trackEvent:AdBreakComplete');
669          _Heartbeat.trackEvent(_MediaHeartbeat.Event.AdBreakComplete);
670          _AdsPlaying = false;
671        }
672      });
673
674      player.on('remove', (data) => {
675        // Close the session if it's still in progress
676        if (_sessionStarted) {
677          log('remove: session in progress. stop it');
678          _Heartbeat.trackEvent(_MediaHeartbeat.Event.ChapterComplete);
679          _Heartbeat.trackComplete();
680          resetSession();
681        } else {
682          log('remove: session not in progress. do nothing.');
683        }
684      });
685    } // setupListeners
686
687    // Main code for plugin
688    //
689    log('initializing heartbeat plugin');
690    setupHeartbeat();
691    setupListeners();
692    log('initializing complete');
693  }; // End of Plugin Code
694
695  // Initialize the plugin
696  //
697  plugin.version = '1.0.0';
698
699  console.log('[jwhb]: RegisterPlugin');
700  const registerPlugin = window.jwplayerPluginJsonp || window.jwplayer().registerPlugin;
701  registerPlugin(id, '8.0.0', plugin);
702}());

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.