UNPKG

npaw-plugin-adapters

Version:
526 lines (457 loc) 17.4 kB
/* global akamai */ export default class AkamaiAdapter { checkExistsPlayer() { try { // Akamai player is a wrapper object, check its media element instead if (this.player && this.player.mediaElement) { return this.checkExistsObjectOnPage(this.player.mediaElement); } // Fallback: if no mediaElement, assume player exists return this.player != null; } catch (err) { return true; } } getVersion() { return '7.0.1-akamai-jsclass'; } getPlayerName() { return 'akamai-amp'; } getPlayerVersion() { var ret = null; if (this.player && typeof this.player.version === 'string') { ret = this.player.version; } return ret; } getPlayhead() { return this.player ? this.player.currentTime : null; } getDuration() { return this.player ? this.player.duration : null; } getPlayrate() { if (this.player && typeof this.player.playbackRate !== 'undefined') { return this.player.playbackRate; } else if (typeof akamai !== 'undefined' && akamai.amp && akamai.amp.playbackRate) { return akamai.amp.playbackRate; } return 1; } getIsLive() { if (this.player && this.player.isLive && typeof this.player.isLive === 'boolean') { return this.player.isLive; } else if (this.player && this.player.temporalType) { return this.player.temporalType === 'live'; } return null; } getResource() { var ret = null; if (this.player && typeof this.player.src === 'string') { ret = this.player.src; } return ret; } getBitrate() { // Try to get bitrate from quality info if (this.currentQuality && this.currentQuality.bitrate) { return this.currentQuality.bitrate; } // Return -1 when bitrate is not available (NPAW spec requirement) // KNOWN LIMITATION: Safari/Webkit doesn't fire quality change events and // doesn't expose HLS quality information through Akamai AMP's API, // so bitrate will report -1 on Safari/iOS return -1; } getRendition() { // Get rendition from cached quality info (updated by quality change events) // KNOWN LIMITATION: On Safari/iOS, quality events don't fire so rendition // may only show resolution (from video element) without bitrate if (this.currentQuality && this.currentQuality.width && this.currentQuality.height) { return this.getNpawUtils().buildRenditionString( this.currentQuality.width, this.currentQuality.height, this.currentQuality.bitrate ); } return null; } getDroppedFrames() { // Try to get dropped frames from player stats if available if (this.player && typeof this.player.getStats === 'function') { try { var stats = this.player.getStats(); if (stats && stats.droppedFrames !== undefined) { return stats.droppedFrames; } } catch (err) { // Ignore errors accessing stats } } return null; } getFramesPerSecond() { if (this.currentQuality && this.currentQuality.frameRate) { return this.currentQuality.frameRate; } return null; } registerListeners() { this.isLive = false; this.currentQuality = null; this.lastResource = null; // KNOWN LIMITATION: Safari/iOS/Webkit // - Quality change events (QUALITY_CHANGE, QUALITY_SWITCHED) don't fire // - HLS quality information is not accessible through Akamai AMP API // - Result: bitrate reports -1, rendition shows resolution only (no bitrate) // - Rendition changes are not detected // Get AMP Events constant if available var Events = typeof akamai !== 'undefined' && akamai.amp && akamai.amp.Events ? akamai.amp.Events : null; this.references = {}; if (Events) { // Use AMP-specific event constants this.references[Events.LOAD_START] = this.loadStartListener.bind(this); this.references[Events.LOADED_METADATA] = this.loadedMetadataListener.bind(this); this.references[Events.LOADED_DATA] = this.loadedDataListener.bind(this); this.references[Events.STARTED] = this.startedListener.bind(this); this.references[Events.PLAYING] = this.playingListener.bind(this); this.references[Events.PAUSED] = this.pausedListener.bind(this); this.references[Events.RESUME] = this.resumeListener.bind(this); this.references[Events.SEEKING] = this.seekingListener.bind(this); this.references[Events.SEEKED] = this.seekedListener.bind(this); this.references[Events.BUFFERING_CHANGE] = this.bufferingChangeListener.bind(this); this.references[Events.WAITING] = this.waitingListener.bind(this); this.references[Events.CAN_PLAY] = this.canPlayListener.bind(this); this.references[Events.ENDED] = this.endedListener.bind(this); this.references[Events.ERROR] = this.errorListener.bind(this); this.references[Events.TIME_UPDATE] = this.timeupdateListener.bind(this); this.references[Events.IS_LIVE] = this.isLiveListener.bind(this); this.references[Events.TEMPORAL_TYPE_CHANGE] = this.temporalTypeChangeListener.bind(this); this.references[Events.MEDIA_CHANGE] = this.mediaChangeListener.bind(this); this.references[Events.QUALITY_CHANGE] = this.qualityChangeListener.bind(this); this.references[Events.QUALITY_SWITCHED] = this.qualitySwitchedListener.bind(this); } // Register all listeners if (this.player) { for (var key in this.references) { this.player.addEventListener(key, this.references[key]); } } } unregisterListeners() { this.isLive = false; this.currentQuality = null; this.lastResource = null; if (this.player && this.references) { for (var key in this.references) { this.player.removeEventListener(key, this.references[key]); } delete this.references; } } // Event Listeners loadStartListener() { this.firePlayerLog('loadStartListener', {}); // Check if this is a new resource var currentResource = this.getResource(); if (this.flags.isStarted && currentResource && this.lastResource && currentResource !== this.lastResource) { // New content loading, fire stop for previous content this.fireStop({}, 'loadStartListener'); } this.isLive = this.getIsLive(); this.lastResource = currentResource; } loadedMetadataListener() { this.firePlayerLog('loadedMetadataListener', {}); // Metadata loaded, can check duration now if (this.player && typeof this.player.absoluteDuration === 'undefined') { this.isLive = true; } } loadedDataListener() { this.firePlayerLog('loadedDataListener', {}); } startedListener() { this.firePlayerLog('startedListener', {}); this.fireStart({}, 'startedListener'); } playingListener() { this.firePlayerLog('playingListener', {}); // Context-dependent: could be join, resume, or buffer end if (!this.flags.isJoined && this.getPlayhead() > 0) { this.fireJoin({}, 'playingListener'); } else if (this.flags.isPaused) { this.fireResume({}, 'playingListener'); } else if (this.flags.isBuffering) { this.fireBufferEnd({}, 'playingListener'); } if (this.flags.isSeeking) { this.fireSeekEnd({}, 'playingListener'); } } pausedListener() { this.firePlayerLog('pausedListener', {}); this.firePause({}, 'pausedListener'); } resumeListener() { this.firePlayerLog('resumeListener', {}); this.fireResume({}, 'resumeListener'); } seekingListener() { this.firePlayerLog('seekingListener', {}); this.fireSeekBegin({}, false, 'seekingListener'); } seekedListener() { this.firePlayerLog('seekedListener', {}); // Only fire seekEnd if we're not still buffering if (!this.flags.isBuffering) { this.fireSeekEnd({}, 'seekedListener'); } } bufferingChangeListener(e) { this.firePlayerLog('bufferingChangeListener', {}); // AMP provides buffering state in event if (e && typeof e.buffering !== 'undefined') { if (e.buffering === true) { // Don't fire buffer begin if we're seeking, paused, or haven't joined yet // Safari fires buffering events during initial load, pause transitions, and seek operations // Check if player is actually in active playback state // Fourth parameter 'true' enables automatic filtering of buffers < 100ms if (this.flags.isJoined && this._isPlayerActuallyPlaying()) { this.fireBufferBegin({}, false, 'bufferingChangeListener', true); } } else if (e.buffering === false) { // Only fire buffer end if we were actually buffering if (this.flags.isBuffering) { this.fireBufferEnd({}, 'bufferingChangeListener'); } } } } waitingListener() { this.firePlayerLog('waitingListener', {}); // Don't fire buffer begin if we're seeking, paused, or haven't joined yet // Safari fires 'waiting' during initial load, pause transitions, and seek operations // Check if player is actually in active playback state // Fourth parameter 'true' enables automatic filtering of buffers < 100ms if (this.flags.isJoined && this._isPlayerActuallyPlaying()) { this.fireBufferBegin({}, false, 'waitingListener', true); } } canPlayListener() { this.firePlayerLog('canPlayListener', {}); // Fire buffer end when player can play again if (this.flags.isBuffering) { this.fireBufferEnd({}, 'canPlayListener'); } } timeupdateListener() { // Use timeupdate for join detection if (!this.flags.isJoined && this.getPlayhead() > 0) { this.fireStart({}, 'timeupdateListener'); this.fireJoin({}, 'timeupdateListener'); // Safari doesn't fire quality events, try to get initial quality info this._tryGetInitialQuality(); } } endedListener() { this.firePlayerLog('endedListener', {}); // Check if there are post-roll ads const adsAdapter = this.getVideo().getAdsAdapter(); let willShowCSAIAds = false; if (adsAdapter && typeof adsAdapter.isDAI !== 'undefined') { if (!adsAdapter.isDAI) { // This means we are using Google IMA with CSAI if (adsAdapter.player && typeof adsAdapter.player.getCuePoints === 'function') { willShowCSAIAds = adsAdapter.player.getCuePoints().includes(-1); } } } if (!willShowCSAIAds) { this.fireStop({}, 'endedListener'); } } errorListener(e) { this.firePlayerLog('errorListener', {}); try { // Defensive: Handle malformed error events if (!e || !e.data) { this.fireError('UNKNOWN_ERROR', 'Malformed error event received', {}, false, 'errorListener'); return; } const code = e.data.code || 'UNKNOWN_ERROR'; const msg = e.data.message || 'Unknown error occurred'; const metadata = e.data.metadata || {}; const isFatal = metadata.fatal === true; // Fatal errors stop the view (stop pings and close the session) // Non-fatal errors just log the error but continue playback if (isFatal || code == '2' || code == '4') { this.fireFatalError(code, msg, metadata, true, 'errorListener'); if (!this.flags.isJoined) { this.fireStop({}, 'errorListener'); } } else { this.fireError(code, msg, metadata, false, 'errorListener'); } } catch (err) { // Last resort: ensure we always report something even if error processing fails try { this.fireError('ERROR_HANDLER_EXCEPTION', 'Exception in error handler: ' + (err.message || 'unknown'), {}, false, 'errorListener'); } catch (finalErr) { // Silent fail - can't do much if error reporting itself fails } } } isLiveListener(e) { this.firePlayerLog('isLiveListener', {}); // Defensive: Handle malformed event if (e && typeof e.data !== 'undefined') { this.isLive = e.data; } } temporalTypeChangeListener() { this.firePlayerLog('temporalTypeChangeListener', {}); // Temporal type changed (VOD <-> Live), treat as new content if (this.flags.isStarted) { this.fireStop({}, 'temporalTypeChangeListener'); } } mediaChangeListener() { this.firePlayerLog('mediaChangeListener', {}); // Media changed, fire stop for old content if (this.flags.isStarted) { this.fireStop({}, 'mediaChangeListener'); } } qualityChangeListener(e) { this.firePlayerLog('qualityChangeListener', { eventData: e.data }); this._updateQualityInfo(e); } qualitySwitchedListener(e) { this.firePlayerLog('qualitySwitchedListener', { eventData: e.data }); this._updateQualityInfo(e); } // Helper Methods _updateQualityInfo(e) { // Get quality info from event data if (!e || !e.data) { return; } const { bitrate, height, width, frameRate } = e.data; // Store previous rendition for change detection const previousRendition = this.getRendition(); // Update current quality this.currentQuality = { bitrate: bitrate || null, height: height || null, width: width || null, frameRate: frameRate || null }; // Detect rendition change and send entities parameter const newRendition = this.getRendition(); if (newRendition && previousRendition && newRendition !== previousRendition) { // Send rendition change via entities parameter (next ping) if (this.storeNewRendition) { this.storeNewRendition(newRendition); } } } _tryGetInitialQuality() { // Try to get initial quality info directly from player // Safari doesn't fire quality change events, so we need to fetch it manually if (this.currentQuality) { return; // Already have quality info } try { // Try getting quality from player's qualityLevels if available if (this.player && this.player.qualityLevels && typeof this.player.qualityLevels === 'object') { const levels = this.player.qualityLevels; if (levels.length > 0 && typeof this.player.quality === 'number') { const currentLevel = levels[this.player.quality]; if (currentLevel) { this.currentQuality = { bitrate: currentLevel.bitrate || null, height: currentLevel.height || null, width: currentLevel.width || null, frameRate: currentLevel.frameRate || null }; return; } } } // Try getting from HLS module (for Safari) if (this.player && this.player.hls) { const hls = this.player.hls; // Try to get current level from HLS if (typeof hls.currentLevel === 'number' && hls.levels && hls.levels.length > 0) { const currentLevel = hls.levels[hls.currentLevel]; if (currentLevel) { this.currentQuality = { bitrate: currentLevel.bitrate || currentLevel.attrs?.BANDWIDTH || null, height: currentLevel.height || null, width: currentLevel.width || null, frameRate: currentLevel.frameRate || currentLevel.attrs?.['FRAME-RATE'] || null }; return; } } } // Try getting from video element properties (fallback) if (this.player && this.player.mediaElement) { const video = this.player.mediaElement; if (video.videoWidth && video.videoHeight) { this.currentQuality = { bitrate: null, // Can't get bitrate from video element alone height: video.videoHeight, width: video.videoWidth, frameRate: null }; } } } catch (err) { // Ignore errors getting initial quality info } } _isPlayerPaused() { // Check actual player paused state // This is needed because Safari fires buffering events during pause transitions // before the adapter's isPaused flag is set if (this.player && typeof this.player.paused !== 'undefined') { return this.player.paused; } return false; } _isPlayerSeeking() { // Check actual player seeking state // This is needed because Safari fires buffering events during seek operations // before the adapter's isSeeking flag is set if (this.player && typeof this.player.seeking !== 'undefined') { return this.player.seeking; } return false; } _isPlayerActuallyPlaying() { // Check if player is in an active playing state // This helps detect if we're in a transitional state (pause/seek starting but not complete) if (!this.player) { return false; } // If paused or seeking, definitely not playing if (this._isPlayerPaused() || this._isPlayerSeeking()) { return false; } // Check AMP's playState if available if (typeof this.player.playState !== 'undefined') { // AMP playState: 'ready', 'playing', 'paused', 'ended', etc. return this.player.playState === 'playing'; } // Check waiting property - if waiting, not actively playing if (typeof this.player.waiting !== 'undefined' && this.player.waiting) { return false; } // Fallback: if not paused and not seeking, assume playing return !this.player.paused && !this.player.seeking; } }