UNPKG

npaw-plugin-adapters

Version:
698 lines (605 loc) 21.7 kB
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; } }