npaw-plugin-adapters
Version:
NPAW's Plugin Adapters
698 lines (605 loc) • 21.7 kB
JavaScript
import NativeAdsAdapter from './ads/NativeAdsAdapter';
export default class ChromecastCafAdapter {
adsAdapters = {
nativeAdsAdapter: NativeAdsAdapter
};
getVersion() {
return '7.0.4-chromecast-caf-jsclass';
}
getPlayhead() {
return this.player.getCurrentTimeSec();
}
getPlayrate() {
let ret = this.player.getPlaybackRate();
if (!ret && !this.flags.isPaused) {
ret = 1;
}
return ret;
}
getDroppedFrames() {
return this.castMediaElement ? this.castMediaElement.webkitDroppedFrameCount : undefined;
}
getDuration() {
return this.player.getDurationSec();
}
getBitrate() {
return this.bitrate || 0;
}
getThroughput() {
let tp = this.throughput || -1;
if (!this.throughput) {
if (
this.castMediaElement &&
this.castMediaElement.webkitVideoDecodedByteCount &&
this.castMediaElement.webkitAudioDecodedByteCount
) {
const video = this.castMediaElement.webkitVideoDecodedByteCount;
const audio = this.castMediaElement.webkitAudioDecodedByteCount;
const prevCount = this.actualCount || 0;
if (video && audio > video) {
this.actualCount = video;
} else {
this.actualCount = audio + video;
}
const pingTime = JSON.parse(this.plugin.getStoredData()).q.pt || 5;
tp = Math.round((this.actualCount - prevCount) / (pingTime / 1000));
}
}
return tp;
}
getRendition() {
const width = this.castMediaElement ? this.castMediaElement.videoWidth : undefined;
const height = this.castMediaElement ? this.castMediaElement.videoHeight : undefined;
return this.getNpawUtils().buildRenditionString(width, height, this.getBitrate());
}
getTitle() {
return this.player.getMediaInformation().metadata.title;
}
getIsLive() {
return this.getDuration() === -1;
}
getResource() {
let ret;
if (this.player.getMediaInformation()) {
ret = this.player.getMediaInformation().contentUrl || this.player.getMediaInformation().contentId;
}
return ret;
}
getPlayerVersion() {
return cast && cast.framework ? cast.framework.VERSION : undefined;
}
getPlayerName() {
return 'Chromecast';
}
getVideoCodec() {
try {
// For DASH (and fMP4 HLS): Try to get codec from Shaka Player's variant tracks
// Get Shaka instance lazily (only available after media loads)
const shakaInstance = this.player && typeof this.player.getShakaPlayer === 'function'
? this.player.getShakaPlayer()
: null;
if (shakaInstance && typeof shakaInstance.getVariantTracks === 'function') {
const variants = shakaInstance.getVariantTracks();
const activeVariant = variants.find(v => v.active);
if (activeVariant && activeVariant.videoCodec) {
return activeVariant.videoCodec;
}
}
// For HLS (MPEG-TS using MPL): Parse manifest for codec info
const mediaInfo = this.player.getMediaInformation();
if (mediaInfo && this._isHLS(mediaInfo.contentType) && !shakaInstance) {
return this._getHLSVideoCodec();
}
// Fallback to track-based detection (for MP4 and simple streams)
const videoTrack = this._getActiveVideoTrack();
if (!videoTrack || !videoTrack.trackContentType) {
return null;
}
return this._parseCodecFromMimeType(videoTrack.trackContentType, 'video');
} catch (e) {
return null;
}
}
getAudioCodec() {
try {
// For DASH (and fMP4 HLS): Try to get codec from Shaka Player's variant tracks
// Get Shaka instance lazily (only available after media loads)
const shakaInstance = this.player && typeof this.player.getShakaPlayer === 'function'
? this.player.getShakaPlayer()
: null;
if (shakaInstance && typeof shakaInstance.getVariantTracks === 'function') {
const variants = shakaInstance.getVariantTracks();
const activeVariant = variants.find(v => v.active);
if (activeVariant && activeVariant.audioCodec) {
return activeVariant.audioCodec;
}
}
// For HLS (MPEG-TS using MPL): Parse manifest for codec info
const mediaInfo = this.player.getMediaInformation();
if (mediaInfo && this._isHLS(mediaInfo.contentType) && !shakaInstance) {
return this._getHLSAudioCodec();
}
// Try CAF's AudioTracksManager API
const audioTrack = this._getActiveAudioTrack();
if (!audioTrack) {
return null;
}
// Check for audioTrackInfo.audioCodec (DASH/HLS with detailed track info)
if (audioTrack.audioTrackInfo && audioTrack.audioTrackInfo.audioCodec) {
return audioTrack.audioTrackInfo.audioCodec;
}
// Fall back to parsing trackContentType
if (audioTrack.trackContentType) {
return this._parseCodecFromMimeType(audioTrack.trackContentType, 'audio');
}
return null;
} catch (e) {
return null;
}
}
_getActiveAudioTrack() {
try {
const audioTracksManager = this.player.getAudioTracksManager();
if (!audioTracksManager) {
return null;
}
const activeTrack = audioTracksManager.getActiveTrack();
// If no active track, check if we have tracks array and use first audio track
if (!activeTrack) {
const mediaInfo = this.player.getMediaInformation();
if (mediaInfo) {
// For simple MP4, use contentType
if (!mediaInfo.tracks || mediaInfo.tracks.length === 0) {
return {
type: 'AUDIO',
trackContentType: mediaInfo.contentType
};
}
// For DASH/HLS, find audio track
const audioTracks = mediaInfo.tracks.filter((track) => track.type === 'AUDIO');
if (audioTracks.length > 0) {
return audioTracks[0];
}
}
}
return activeTrack;
} catch (e) {
return null;
}
}
_getActiveVideoTrack() {
try {
const mediaInfo = this.player.getMediaInformation();
if (!mediaInfo) {
return null;
}
// For simple MP4/media without tracks, use contentType directly
if (!mediaInfo.tracks || mediaInfo.tracks.length === 0) {
return {
type: 'VIDEO',
trackContentType: mediaInfo.contentType
};
}
// For DASH/HLS with tracks, find active video track
const videoTracks = mediaInfo.tracks.filter((track) => track.type === 'VIDEO');
// Return first video track (or could check activeTrackIds if method exists)
return videoTracks.length > 0 ? videoTracks[0] : null;
} catch (e) {
return null;
}
}
_parseCodecFromMimeType(mimeType, mediaType) {
if (!mimeType) return null;
// Extract codecs parameter: codecs="xxx" or codecs='xxx'
const codecsMatch = mimeType.match(/codecs=["']?([^"']+)["']?/i);
if (!codecsMatch) return null;
const codecs = codecsMatch[1];
// Split multiple codecs (e.g., "avc1.64002a, mp4a.40.2")
const codecArray = codecs.split(',').map((c) => c.trim());
// Filter by media type
if (mediaType === 'video') {
// Video codecs typically start with: avc1, hvc1, hev1, vp8, vp9, av01
return (
codecArray.find(
(c) =>
c.startsWith('avc') ||
c.startsWith('hvc') ||
c.startsWith('hev') ||
c.startsWith('vp') ||
c.startsWith('av0')
) || null
);
} else if (mediaType === 'audio') {
// Audio codecs typically start with: mp4a, opus, vorbis, ac-3, ec-3
return (
codecArray.find(
(c) =>
c.startsWith('mp4a') ||
c.startsWith('opus') ||
c.startsWith('vorbis') ||
c.startsWith('ac-3') ||
c.startsWith('ec-3') ||
c.startsWith('dtsc') ||
c.startsWith('dtse')
) || null
);
}
return codecs; // Return full string as fallback
}
_isHLS(contentType) {
return contentType === 'application/x-mpegURL' ||
contentType === 'application/vnd.apple.mpegurl';
}
_getHLSVideoCodec() {
if (!this._hlsVariants) {
this._parseHLSManifest();
}
if (!this._hlsVariants || this._hlsVariants.length === 0) {
return null;
}
// Match current bitrate to variant
const currentBitrate = this.getBitrate();
if (!currentBitrate) {
return this._hlsVariants[0]?.videoCodec || null;
}
// Find closest matching variant by bandwidth
const variant = this._hlsVariants.reduce((closest, v) => {
if (!closest) return v;
const closestDiff = Math.abs(closest.bandwidth - currentBitrate);
const vDiff = Math.abs(v.bandwidth - currentBitrate);
return vDiff < closestDiff ? v : closest;
}, null);
return variant?.videoCodec || null;
}
_getHLSAudioCodec() {
if (!this._hlsVariants) {
this._parseHLSManifest();
}
if (!this._hlsVariants || this._hlsVariants.length === 0) {
return null;
}
// Match current bitrate to variant
const currentBitrate = this.getBitrate();
if (!currentBitrate) {
return this._hlsVariants[0]?.audioCodec || null;
}
// Find closest matching variant by bandwidth
const variant = this._hlsVariants.reduce((closest, v) => {
if (!closest) return v;
const closestDiff = Math.abs(closest.bandwidth - currentBitrate);
const vDiff = Math.abs(v.bandwidth - currentBitrate);
return vDiff < closestDiff ? v : closest;
}, null);
return variant?.audioCodec || null;
}
_parseHLSManifest() {
try {
const mediaInfo = this.player.getMediaInformation();
const manifestUrl = mediaInfo?.contentId;
if (!manifestUrl) {
return;
}
// Use XHR to fetch manifest
const xhr = new XMLHttpRequest();
xhr.open('GET', manifestUrl, false); // Synchronous for simplicity
xhr.send();
if (xhr.status !== 200) {
return;
}
const manifestText = xhr.responseText;
this._hlsVariants = this._extractHLSVariants(manifestText);
} catch (e) {
// Silently fail - will return null codecs
}
}
_extractHLSVariants(manifestText) {
const variants = [];
const lines = manifestText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Look for #EXT-X-STREAM-INF lines
if (line.startsWith('#EXT-X-STREAM-INF:')) {
// Extract BANDWIDTH
const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : null;
// Extract CODECS
const codecsMatch = line.match(/CODECS="([^"]+)"/);
const codecs = codecsMatch ? codecsMatch[1] : null;
if (bandwidth && codecs) {
// Split codecs: "avc1.64002a,mp4a.40.2" -> video and audio
const codecArray = codecs.split(',').map(c => c.trim());
const videoCodec = codecArray.find(c =>
c.startsWith('avc') || c.startsWith('hvc') || c.startsWith('hev') ||
c.startsWith('vp') || c.startsWith('av0')
) || null;
const audioCodec = codecArray.find(c =>
c.startsWith('mp4a') || c.startsWith('opus') || c.startsWith('vorbis') ||
c.startsWith('ac-3') || c.startsWith('ec-3')
) || null;
variants.push({
bandwidth,
videoCodec,
audioCodec
});
}
}
}
return variants;
}
registerListeners() {
this.castMediaElement = undefined;
for (let object in this.player) {
if (typeof this.player[object] === 'object') {
for (let object2 in this.player[object]) {
// Find castMediaElement
if (this.player[object][object2] && this.player[object][object2].id === 'castMediaElement') {
this.castMediaElement = this.player[object][object2];
}
}
}
}
const Events = cast.framework.events;
const eCode = cast.framework.events.DetailedErrorCode;
this.errorFilter = [
eCode.MEDIAKEYS_UNKNOWN,
eCode.MEDIAKEYS_UNSUPPORTED,
eCode.MEDIAKEYS_WEBCRYPTO,
eCode.SEGMENT_NETWORK,
eCode.HLS_NETWORK_INVALID_SEGMENT,
eCode.DASH_MANIFEST_NO_MIMETYPE,
eCode.DASH_INVALID_SEGMENT_INFO,
eCode.TEXT_UNKNOWN,
eCode.SEGMENT_UNKNOWN,
eCode.BREAK_SEEK_INTERCEPTOR_ERROR,
eCode.APP,
eCode.IMAGE_ERROR,
eCode.LOAD_INTERRUPTED,
eCode.MEDIA_ERROR_MESSAGE
];
this.getNpawUtils().logAllEvents(this.player, [
null,
Events.EventType.PLAY,
Events.EventType.PAUSE,
Events.EventType.PLAYING,
Events.EventType.STALLED,
Events.EventType.SEEKING,
Events.EventType.SEEKED,
Events.EventType.ERROR,
Events.EventType.CLIP_ENDED,
Events.EventType.BITRATE_CHANGED,
Events.EventType.SEGMENT_DOWNLOADED,
Events.EventType.BUFFERING,
Events.EventType.EMSG,
Events.EventType.ID3,
Events.EventType.MEDIA_FINISHED,
Events.EventType.MEDIA_STATUS,
Events.EventType.CACHE_HIT,
Events.EventType.BREAK_CLIP_STARTED,
Events.EventType.BREAK_ENDED,
Events.EventType.BREAK_CLIP_LOADING,
Events.EventType.BREAK_CLIP_STARTED,
Events.EventType.BREAK_CLIP_ENDED,
Events.EventType.BREAK_STARTED
]);
this.monitorPlayhead(true, true);
this.references = {};
this.references[Events.EventType.PLAY] = this.playListener.bind(this);
this.references[Events.EventType.STALLED] = this.playListener.bind(this);
this.references[Events.EventType.BUFFERING] = this.bufferingListener.bind(this);
this.references[Events.EventType.PAUSE] = this.pauseListener.bind(this);
this.references[Events.EventType.BREAK_CLIP_STARTED] = this.pauseListener.bind(this);
this.references[Events.EventType.BREAK_ENDED] = this.resumeListener.bind(this);
this.references[Events.EventType.PLAYING] = this.playingListener.bind(this);
this.references[Events.EventType.SEEKING] = this.seekingListener.bind(this);
this.references[Events.EventType.MEDIA_FINISHED] = this.endedListener.bind(this);
this.references[Events.EventType.ERROR] = this.errorListener.bind(this);
this.references[Events.EventType.BITRATE_CHANGED] = this.bitrateChangedListener.bind(this);
this.references[Events.EventType.SEGMENT_DOWNLOADED] = this.segmentListener.bind(this);
this.references[Events.EventType.BREAK_STARTED] = this.playAdListener.bind(this);
this.references[Events.EventType.TIME_UPDATE] = this.timeUpdateListener.bind(this);
this.hostReferences = {
onAutoPause: this.autoPauseListener.bind(this)
};
for (var key in this.references) {
this.player.addEventListener(key, this.references[key]);
}
if (
cast &&
cast.player &&
cast.player.api &&
cast.player.api.Player &&
cast.player.api.Player.getHost &&
cast.player.api.Player.getHost()
) {
for (var key2 in this.hostReferences) {
cast.player.api.Player.getHost().addEventListener(key2, this.hostReferences[key2]);
}
}
if (!this.getVideo().getAdsAdapter()) {
this.getAdapterClass('nativeAdsAdapter')._updatePlayer(this.player);
this.plugin.setAdsAdapter(this.getAdapterClass('nativeAdsAdapter'), this.getVideo().getVideoKey());
}
this.registerModelVersion();
}
unregisterListeners() {
if (this.monitor) this.monitor.stop();
if (this.player && this.references) {
for (var key in this.references) {
this.player.removeEventListener(key, this.references[key]);
}
this.references = {};
if (
cast &&
cast.player &&
cast.player.api &&
cast.player.api.Player &&
cast.player.api.Player.getHost &&
cast.player.api.Player.getHost() &&
this.hostReferences
) {
for (var key2 in this.hostReferences) {
this.player.getHost().removeEventListener(key2, this.hostReferences[key2]);
}
this.hostReferences = {};
}
}
}
registerModelVersion() {
// If client set model on plugin options we do not override
if (typeof this.getVideo().options['device.model'] !== 'undefined') {
return;
}
// https://developers.google.com/cast/docs/media#video_codecs
try {
const { hardwareConcurrency, userAgent } = window.navigator;
const context = cast.framework.CastReceiverContext.getInstance();
// Chromecast with Google TV supports 'H.264 High Profile, level 5.1'
if (context.canDisplayType('video/mp4; codecs="avc1.640033')) {
this.getVideo().options['device.model'] = 'Chromecast with Google TV';
return;
}
// Android Devices with Chromecast built-in
if (userAgent.includes('Android')) {
this.getVideo().options['device.model'] = 'Chromecast Android';
return;
}
// Chromecast Ultra supports 'HEVC main profile, level 3.1'
if (context.canDisplayType('video/mp4; codecs=hev1.1.6.L93.B0')) {
this.getVideo().options['device.model'] = 'Chromecast Ultra';
return;
}
// 3rd generation Chromecast supports 'H.264 high profile, level 4.2'
if (context.canDisplayType('video/mp4; codecs=avc1.64002A')) {
this.getVideo().options['device.model'] = 'Chromecast 3';
return;
}
// 2nd and 1st generation Chromecast can be differentiated by hardwareConcurrency
if (hardwareConcurrency === 2) {
this.getVideo().options['device.model'] = 'Chromecast 2';
return;
}
if (hardwareConcurrency === 1) {
this.getVideo().options['device.model'] = 'Chromecast 1';
return;
}
} catch (e) {
// do nothing
}
this.getVideo().options['device.model'] = 'Chromecast';
}
playListener(e) {
if (!this.flags.isStarted) {
this.lastPlayhead = undefined;
this.fireStart({}, 'playListener');
}
}
playAdListener(e) {
this.playListener();
this.firePause({}, 'playAdListener');
}
bufferingListener(e) {
if (this.flags.isPaused) {
if (!this.plugin || !this.getVideo().isBreakStarted) {
this.fireResume({}, 'bufferingListener');
this.fireBufferBegin({}, false, 'bufferingListener');
}
} else {
this.fireBufferBegin({}, false, 'bufferingListener');
}
if (!this.flags.isStarted) {
this.lastPlayhead = undefined;
this.fireStart({}, 'bufferingListener');
}
}
pauseListener(e) {
if (!this.flags.isBuffering) {
this.firePause({}, 'pauseListener');
}
}
resumeListener(e) {
this.fireResume({}, 'resumeListener');
}
playingListener(e) {
if (!this.plugin || !this.getVideo().isBreakStarted) {
this.fireResume({}, 'playingListener');
this.fireBufferEnd({}, 'playingListener');
this.fireJoin({}, 'playingListener');
}
}
errorListener(e) {
const error = e.error;
const code = e.detailedErrorCode;
if (code) {
if (this.errorFilter.indexOf(code) >= 0) {
return;
}
const eCode = cast.framework.events.DetailedErrorCode;
let message =
Object.keys(eCode).find(function (key) {
return eCode[key] === code;
}) || code;
if (error && error.shakaErrorCode) {
message = 'Shaka: ' + error.shakaErrorCode + ' , ' + message;
}
if (error === 'LOAD_CANCELLED') {
this.fireFatalError(code, message);
this._reset();
} else {
this.fireError(code, message);
}
}
}
seekingListener(e) {
this.fireSeekBegin({}, false, 'seekingListener');
}
endedListener(e) {
this.fireStop({}, 'endedListener');
this._reset();
}
bitrateChangedListener(e) {
this.bitrate = e.totalBitrate;
// Check for codec changes during ABR switching
const currentVideoCodec = this.getVideoCodec();
const currentAudioCodec = this.getAudioCodec();
if (currentVideoCodec && currentVideoCodec !== this._lastVideoCodec) {
this.storeNewRendition();
this._lastVideoCodec = currentVideoCodec;
}
if (currentAudioCodec && currentAudioCodec !== this._lastAudioCodec) {
this.storeNewRendition();
this._lastAudioCodec = currentAudioCodec;
}
}
segmentListener(e) {
this.throughput = e.size / (e.downloadTime / 1000);
}
timeUpdateListener(e) {
if (!this.flags.isJoined) {
if (this.lastPlayhead === null || this.lastPlayhead === undefined) {
this.lastPlayhead = this.getPlayhead();
} else if (
this.lastPlayhead < this.getPlayhead() &&
this.plugin &&
(!this.plugin.getAdsAdapter() || (this.plugin.getAdsAdapter() && !this.plugin.getAdsAdapter().flags.isStarted))
) {
this.fireJoin({}, 'timeUpdateListener');
}
}
}
autoPauseListener(e) {
if (e) {
this.fireResume({}, 'autoPauseListener');
this.fireBufferBegin({}, false, 'autoPauseListener');
} else {
this.fireBufferEnd({}, 'autoPauseListener');
}
}
_reset(e) {
this.lastPlayhead = undefined;
this.actualCount = 0;
this.bitrate = undefined;
this._lastVideoCodec = null;
this._lastAudioCodec = null;
this._hlsVariants = null;
}
}