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.