npaw-plugin-adapters
Version:
NPAW's Plugin Adapters
526 lines (457 loc) • 17.4 kB
JavaScript
/* 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;
}
}