/** * SABR Player - Main player initialization for SABR streaming * * Re-engineered (from the kira-based POC) to use FreeTube's SABR engine: * - Builds a data:application/sabr+json manifest from the youtube.js VideoInfo * - Registers FreeTube's sabr: networking scheme (sabr_scheme_plugin.js) * - Drives Shaka 5 via the application/sabr+json manifest parser * * PoToken is minted browser-side via bgutils-js BotGuard and proxied through * the Invidious /proxy route for CSP compliance. */ 'use strict'; var SABRPlayer = (function () { var VOLUME_KEY = 'youtube_player_volume'; var PLAYBACK_POSITION_KEY = 'youtube_playback_positions'; var SAVE_POSITION_INTERVAL_MS = 5000; var DEFAULT_ABR_CONFIG = { enabled: true, restrictions: { maxHeight: 480 }, switchInterval: 4, useNetworkInformation: false }; // State var player = null; var ui = null; var videoElement = null; var shakaContainer = null; var currentVideoId = ''; var isLive = false; var innertube = null; var clientConfig = null; var savePositionInterval = null; var playbackWebPoToken = null; var coldStartToken = null; var playbackWebPoTokenContentBinding = null; var playbackWebPoTokenCreationLock = false; var sabrStream = null; // handle returned by setupSabrScheme var sabrManifest = null; // captured from player.getManifest() on 'loaded' var currentLoadOptions = null; // for onReloadOnce -> re-run loadVideo function getSavedVolume() { try { var v = localStorage.getItem(VOLUME_KEY); return v ? parseFloat(v) : 1; } catch (e) { return 1; } } function saveVolume(volume) { try { localStorage.setItem(VOLUME_KEY, volume.toString()); } catch (e) {} } function getPlaybackPositions() { try { var p = localStorage.getItem(PLAYBACK_POSITION_KEY); return p ? JSON.parse(p) : {}; } catch (e) { return {}; } } function savePlaybackPosition(videoId, time) { if (!videoId || time < 1) return; try { var positions = getPlaybackPositions(); positions[videoId] = time; localStorage.setItem(PLAYBACK_POSITION_KEY, JSON.stringify(positions)); } catch (e) {} } function getPlaybackPosition(videoId) { var positions = getPlaybackPositions(); return positions[videoId] || 0; } function base64ToU8(base64) { // base64ToU8 from googlevideo handles websafe, but for client config we need raw. var binary = atob(base64); var bytes = new Uint8Array(binary.length); for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } async function initInnertube() { if (innertube) return innertube; try { console.info('[SABRPlayer]', 'Initializing InnerTube API'); if (typeof Innertube === 'undefined') throw new Error('youtubei.js not loaded'); // Set up Platform.shim.eval for URL deciphering (browser bundle lacks Jinter). if (typeof Platform !== 'undefined' && Platform.shim) { Platform.shim.eval = async function (data, env) { var properties = []; if (env.n) properties.push('n: exportedVars.nFunction("' + env.n + '")'); if (env.sig) properties.push('sig: exportedVars.sigFunction("' + env.sig + '")'); var code = data.output + '\nreturn { ' + properties.join(', ') + ' }'; return new Function(code)(); }; console.info('[SABRPlayer]', 'Platform.shim.eval configured for URL deciphering'); } else { console.warn('[SABRPlayer]', 'Platform.shim not available, URL deciphering may fail'); } innertube = await Innertube.create({ fetch: SABRHelpers.fetchWithProxy, retrieve_player: true, generate_session_locally: true }); // Kick off BotGuard init (don't block player setup). BotguardService.init().then(function () { console.info('[SABRPlayer]', 'BotGuard client initialized'); }).catch(function (err) { console.warn('[SABRPlayer]', 'BotGuard initialization failed:', err.message); }); // Preload the redirector URL. try { var redirectorResponse = await SABRHelpers.fetchWithProxy( 'https://redirector.googlevideo.com/initplayback?source=youtube&itag=0&pvi=0&pai=0&owc=yes&cmo:sensitive_content=yes&alr=yes&id=' + Math.round(Math.random() * 1e5), { method: 'GET' } ); var redirectorResponseUrl = await redirectorResponse.text(); if (redirectorResponseUrl.indexOf('https://') === 0) { localStorage.setItem(SABRHelpers.REDIRECTOR_STORAGE_KEY, redirectorResponseUrl); } } catch (e) { console.warn('[SABRPlayer]', 'Failed to preload redirector URL', e); } return innertube; } catch (error) { console.error('[SABRPlayer]', 'Failed to initialize Innertube', error); return null; } } async function fetchOnesieConfig() { if (clientConfig && SABRHelpers.isConfigValid(clientConfig)) return clientConfig; var cached = SABRHelpers.loadCachedClientConfig(); if (cached) { clientConfig = cached; return clientConfig; } try { var tvConfigResponse = await SABRHelpers.fetchWithProxy( 'https://www.youtube.com/tv_config?action_get_config=true&client=lb4&theme=cl', { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version' } } ); var tvConfigText = await tvConfigResponse.text(); var tvConfigJson = JSON.parse(tvConfigText.slice(4)); var webPlayerContextConfig = tvConfigJson.webPlayerContextConfig.WEB_PLAYER_CONTEXT_CONFIG_ID_LIVING_ROOM_WATCH; var onesieHotConfig = webPlayerContextConfig.onesieHotConfig; clientConfig = { clientKeyData: base64ToU8(onesieHotConfig.clientKey), keyExpiresInSeconds: onesieHotConfig.keyExpiresInSeconds, encryptedClientKey: base64ToU8(onesieHotConfig.encryptedClientKey), onesieUstreamerConfig: base64ToU8(onesieHotConfig.onesieUstreamerConfig), baseUrl: onesieHotConfig.baseUrl, timestamp: Date.now() }; SABRHelpers.saveCachedClientConfig(clientConfig); return clientConfig; } catch (error) { console.error('[SABRPlayer]', 'Failed to fetch Onesie client config', error); return null; } } async function mintContentWebPO() { if (!playbackWebPoTokenContentBinding || playbackWebPoTokenCreationLock) return; playbackWebPoTokenCreationLock = true; try { coldStartToken = BotguardService.mintColdStartToken(playbackWebPoTokenContentBinding); console.info('[SABRPlayer]', 'Cold start token created:', coldStartToken ? coldStartToken.substring(0, 30) + '...' : 'null'); if (!BotguardService.isInitialized()) { await BotguardService.reinit(); } if (BotguardService.isInitialized()) { playbackWebPoToken = await BotguardService.mintWebPoToken(decodeURIComponent(playbackWebPoTokenContentBinding)); console.info('[SABRPlayer]', 'WebPO token created:', playbackWebPoToken ? playbackWebPoToken.substring(0, 30) + '...' : 'null'); } else { console.warn('[SABRPlayer]', 'BotGuard still not initialized after reinit'); } } catch (err) { console.error('[SABRPlayer]', 'Error minting WebPO token', err); } finally { playbackWebPoTokenCreationLock = false; } } async function initializeShakaPlayer(containerElement, listenMode) { if (!shaka.Player.isBrowserSupported()) { throw new Error('Shaka Player is not supported in this browser'); } shaka.polyfill.installAll(); shakaContainer = document.createElement('div'); shakaContainer.className = 'sabr-player-container'; shakaContainer.style.width = '100%'; shakaContainer.style.height = '100%'; videoElement = document.createElement('video'); videoElement.autoplay = true; videoElement.style.width = '100%'; videoElement.style.height = '100%'; videoElement.style.backgroundColor = '#000'; shakaContainer.appendChild(videoElement); containerElement.appendChild(shakaContainer); player = new shaka.Player(); player.configure({ abr: DEFAULT_ABR_CONFIG, streaming: { bufferingGoal: 120, rebufferingGoal: 0.01, bufferBehind: 300, retryParameters: { maxAttempts: 8, fuzzFactor: 0.5, timeout: 30 * 1000 } }, manifest: { // disableVideo is read by our SabrManifestParser to skip video streams for audio-only/listen mode. disableVideo: !!listenMode } }); videoElement.volume = getSavedVolume(); videoElement.addEventListener('volumechange', function () { saveVolume(videoElement.volume); }); videoElement.addEventListener('playing', function () { player.configure('abr.restrictions.maxHeight', Infinity); }); videoElement.addEventListener('pause', function () { if (currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime); }); await player.attach(videoElement); if (shaka.ui && shaka.ui.Overlay) { ui = new shaka.ui.Overlay(player, shakaContainer, videoElement); ui.configure({ overflowMenuButtons: ['captions', 'quality', 'language', 'playback_rate', 'loop', 'picture_in_picture'] }); } return player; } // Route non-sabr: requests (captions, storyboards, image tiles) through the // Invidious /proxy so they satisfy CSP. The sabr: scheme is handled // separately by setupSabrScheme and also routes through /proxy internally. function setupRequestFilters() { var networkingEngine = player && player.getNetworkingEngine ? player.getNetworkingEngine() : null; if (!networkingEngine) return; networkingEngine.registerRequestFilter(function (type, request) { var uri = request.uris[0]; if (!uri) return; // sabr: is owned by the SABR scheme plugin; never rewrite it here. if (uri.indexOf('sabr:') === 0) return; try { var url = new URL(uri); } catch (e) { return; } var isGoogleVideo = url.hostname.endsWith('.googlevideo.com') || url.hostname.indexOf('googlevideo') !== -1; var isYouTube = url.hostname.endsWith('.youtube.com'); if (!isGoogleVideo && !isYouTube) return; // Reuse the shared proxy helper so __host/__headers are set consistently. var proxied = SABRHelpers.proxyUrl(url, request.headers || {}); proxied.searchParams.set('alr', 'yes'); request.uris[0] = proxied.toString(); }); } // Map a youtube.js Format to the SabrManifest "formats" entry shape expected // by sabr_manifest_parser.js (port of FreeTube's SabrManifestParser). function mapFormatToManifestEntry(fmt) { var audioTrack = fmt.audio_track || null; var colorInfo = fmt.color_info || null; return { itag: fmt.itag, lastModified: fmt.last_modified_ms, mimeType: fmt.mime_type, xtags: fmt.xtags, bitrate: fmt.bitrate, initRange: fmt.init_range, indexRange: fmt.index_range, width: fmt.width, height: fmt.height, frameRate: fmt.fps, quality: fmt.quality, language: fmt.language, audioSampleRate: fmt.audio_sample_rate, audioChannels: fmt.audio_channels, isDrc: fmt.is_drc, isVoiceBoost: fmt.is_vb, isOriginal: fmt.is_original, isDubbed: fmt.is_dubbed, isAutoDubbed: fmt.is_auto_dubbed, isDescriptive: fmt.is_descriptive, isSecondary: fmt.is_secondary, spatialAudio: !!(fmt.spatial_audio_type), label: audioTrack ? audioTrack.display_name : undefined, colorTransferCharacteristics: colorInfo ? colorInfo.transfer_characteristics : undefined, colorPrimaries: colorInfo ? colorInfo.primaries : undefined }; } function buildCaptions(videoInfo) { var out = []; var tracks = videoInfo.captions && videoInfo.captions.caption_tracks; if (!tracks) return out; for (var i = 0; i < tracks.length; i++) { var c = tracks[i]; var url; try { url = new URL(c.base_url); url.searchParams.set('fmt', 'vtt'); url = url.toString(); } catch (e) { url = c.base_url; } out.push({ id: c.vss_id, url: url, label: c.name ? c.name.text : (c.language_code || ''), language: c.language_code || 'und', mimeType: 'text/vtt' }); } return out; } function buildStoryboards(videoInfo) { var out = []; var sb = videoInfo.storyboards; if (!sb || sb.type !== 'PlayerStoryboardSpec') return out; var boards = sb.boards || []; // Pick the highest-res storyboard (matches FreeTube behaviour). var board = boards.length ? boards[boards.length - 1] : null; if (!board) return out; out.push({ templateUrl: board.template_url, mimeType: 'image/webp', columns: board.columns, rows: board.rows, thumbnailCount: board.thumbnail_count, thumbnailWidth: board.thumbnail_width, thumbnailHeight: board.thumbnail_height, storyboardCount: board.storyboard_count, interval: board.interval > 0 ? board.interval / 1000 : 0 }); return out; } function buildChapters(videoInfo) { var out = []; var ch = videoInfo.chapters; if (!ch || !ch.get) return out; try { var list = ch.get(); if (!list || !list.length) return out; for (var i = 0; i < list.length; i++) { var c = list[i]; out.push({ title: c.title || '', startSeconds: c.start || 0, endSeconds: c.end || (i + 1 < list.length ? (list[i + 1].start || 0) : 0) }); } } catch (e) { // chapters not available for this video - harmless } return out; } // Build a data:application/sabr+json manifest URI from a youtube.js VideoInfo. // Port of FreeTube's Watch.js#createLocalSabrManifest. function buildSabrManifest(videoInfo, poToken, clientInfo) { var streamingData = videoInfo.streaming_data; if (!streamingData || !streamingData.server_abr_streaming_url || !streamingData.adaptive_formats) { return null; } var url = new URL(streamingData.server_abr_streaming_url); url.searchParams.set('alr', 'yes'); if (videoInfo.cpn) url.searchParams.set('cpn', videoInfo.cpn); var sabrData = { url: url.toString(), poToken: poToken || '', ustreamerConfig: videoInfo.player_config && videoInfo.player_config.media_common_config && videoInfo.player_config.media_common_config.media_ustreamer_request_config && videoInfo.player_config.media_common_config.media_ustreamer_request_config.video_playback_ustreamer_config || '', clientInfo: clientInfo }; SABRPlayer._lastSabrData = sabrData; var adaptiveFormats = streamingData.adaptive_formats; var duration = Infinity; for (var i = 0; i < adaptiveFormats.length; i++) { var d = adaptiveFormats[i].approx_duration_ms; if (typeof d === 'number' && d < duration) duration = d; } if (!isFinite(duration)) duration = (videoInfo.basic_info && videoInfo.basic_info.duration) ? videoInfo.basic_info.duration * 1000 : 0; duration = duration / 1000; var formats = []; for (var j = 0; j < adaptiveFormats.length; j++) { formats.push(mapFormatToManifestEntry(adaptiveFormats[j])); } var sabrManifest = { duration: duration, formats: formats, captions: buildCaptions(videoInfo), storyboards: buildStoryboards(videoInfo), chapters: buildChapters(videoInfo) }; return 'data:' + window.MANIFEST_TYPE_SABR + ',' + encodeURIComponent(JSON.stringify(sabrManifest)); } function getPlayerWidth() { return videoElement ? videoElement.clientWidth : 1280; } function getPlayerHeight() { return videoElement ? videoElement.clientHeight : 720; } function getPlayer() { return player; } function getManifest() { return sabrManifest; } function wireSabrStream(loadFn) { if (!sabrStream) return; sabrStream.onBackoffRequested(function (info) { var ms = info && info.backoffMs ? info.backoffMs : 0; console.warn('[SABRPlayer] SABR backoff requested:', ms, 'ms'); var toast = document.querySelector('.sabr-backoff-toast'); if (!toast && shakaContainer) { toast = document.createElement('div'); toast.className = 'sabr-backoff-toast'; toast.style.cssText = 'position:absolute;bottom:48px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.75);color:#fff;padding:6px 12px;border-radius:4px;font-size:13px;pointer-events:none;z-index:10'; shakaContainer.appendChild(toast); } if (toast) { toast.textContent = 'SABR throttled by YouTube, retrying in ' + (ms / 1000).toFixed(1) + 's…'; clearTimeout(toast._t); toast._t = setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, Math.max(ms + 500, 2000)); } }); sabrStream.onReloadOnce(function () { console.warn('[SABRPlayer] SABR reload requested by server; re-loading video'); if (currentVideoId && loadFn) loadFn(currentVideoId, shakaContainer, currentLoadOptions || {}); }); } async function loadVideo(videoId, containerElement, options) { options = options || {}; currentVideoId = videoId; currentLoadOptions = options; playbackWebPoToken = null; playbackWebPoTokenContentBinding = videoId; try { if (!innertube) { innertube = await initInnertube(); if (!innertube) throw new Error('Failed to initialize Innertube'); } if (!clientConfig) { await fetchOnesieConfig(); } // Mint a content-bound PoToken before getInfo so streaming_data includes // the server_abr_streaming_url and so the SABR requests authenticate. try { await mintContentWebPO(); } catch (e) { console.warn('[SABRPlayer] poToken mint failed, continuing', e); } var poToken = playbackWebPoToken || coldStartToken || ''; if (!player) { await initializeShakaPlayer(containerElement, options.listen); } else { player.configure('abr', DEFAULT_ABR_CONFIG); player.configure('manifest.disableVideo', !!options.listen); } setupRequestFilters(); var videoInfo = await innertube.getInfo(videoId, { po_token: poToken || undefined }); if (!videoInfo || !videoInfo.playability_status || videoInfo.playability_status.status !== 'OK') { var reason = (videoInfo && videoInfo.playability_status && videoInfo.playability_status.reason) || 'Unknown error'; throw new Error('Video unavailable: ' + reason); } isLive = !!(videoInfo.basic_info && videoInfo.basic_info.is_live); var streamingData = videoInfo.streaming_data; if (!streamingData) throw new Error('No streaming data available'); // Derive clientInfo from the innertube session (matches FreeTube's local.js). var ctxClient = innertube.session && innertube.session.context && innertube.session.context.client; var clientName = ctxClient ? ctxClient.clientName : 'ANDROID'; var clientInfo = { clientName: (typeof Constants !== 'undefined' && Constants.CLIENT_NAME_IDS && Constants.CLIENT_NAME_IDS[clientName]) || 3, clientVersion: ctxClient ? ctxClient.clientVersion : (Constants.CLIENTS && Constants.CLIENTS.ANDROID ? Constants.CLIENTS.ANDROID.VERSION : '19.09.37'), osName: ctxClient ? ctxClient.osName : 'Android', osVersion: ctxClient ? ctxClient.osVersion : '14' }; var manifestUri; if (isLive) { // Live: don't route through the SABR parser yet; use HLS/DASH. manifestUri = streamingData.hls_manifest_url || streamingData.dash_manifest_url; if (streamingData.hls_manifest_url) { if (innertube.session && innertube.session.player && innertube.session.player.decipher) { try { streamingData.hls_manifest_url = await innertube.session.player.decipher(streamingData.hls_manifest_url); } catch (e) {} } manifestUri = streamingData.hls_manifest_url; } } else { if (streamingData.server_abr_streaming_url && innertube.session && innertube.session.player && innertube.session.player.decipher) { try { streamingData.server_abr_streaming_url = await innertube.session.player.decipher(streamingData.server_abr_streaming_url); } catch (e) { console.warn('[SABRPlayer] Failed to decipher server_abr_streaming_url', e); } } manifestUri = buildSabrManifest(videoInfo, poToken, clientInfo); if (!manifestUri) { // No SABR URL available - fall back to DASH. var dashManifest = await videoInfo.toDash({ manifest_options: { captions_format: 'vtt', include_thumbnails: false } }); manifestUri = 'data:application/dash+xml;base64,' + btoa(dashManifest); } } if (!manifestUri) throw new Error('Could not find a valid manifest URI'); // Register the sabr: scheme before loading (idempotent re-registration is fine). if (!isLive && window.setupSabrScheme && SABRPlayer._lastSabrData) { if (sabrStream) { try { sabrStream.cleanup(); } catch (e) {} } sabrStream = window.setupSabrScheme(SABRPlayer._lastSabrData, getPlayer, getManifest, getPlayerWidth, getPlayerHeight); wireSabrStream(loadVideo); } // Capture the parsed manifest once Shaka has loaded it, so the sabr: // scheme plugin can read variant/segment indices from it. player.addEventListener('loaded', function () { if (typeof player.getManifest === 'function') { sabrManifest = player.getManifest(); } }); var startTime = options.startTime; if (startTime === undefined && options.savePlayerPos !== false) { startTime = getPlaybackPosition(videoId); } var mimeType = (!isLive && manifestUri.indexOf('data:' + window.MANIFEST_TYPE_SABR) === 0) ? window.MANIFEST_TYPE_SABR : undefined; await player.load(manifestUri, isLive ? undefined : startTime, mimeType); videoElement.play().catch(function (err) { if (err.name === 'NotAllowedError') console.warn('[SABRPlayer]', 'Autoplay was prevented by the browser'); }); if (savePositionInterval) clearInterval(savePositionInterval); savePositionInterval = setInterval(function () { if (videoElement && currentVideoId && !videoElement.paused) { savePlaybackPosition(currentVideoId, videoElement.currentTime); } }, SAVE_POSITION_INTERVAL_MS); return { player: player, videoElement: videoElement, videoInfo: videoInfo }; } catch (error) { console.error('[SABRPlayer]', 'Error loading video:', error); throw error; } } async function dispose() { if (savePositionInterval) { clearInterval(savePositionInterval); savePositionInterval = null; } if (videoElement && currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime); if (sabrStream) { try { sabrStream.cleanup(); } catch (e) {} sabrStream = null; } if (player) { await player.destroy(); player = null; } if (ui) { ui.destroy(); ui = null; } if (shakaContainer && shakaContainer.parentNode) shakaContainer.parentNode.removeChild(shakaContainer); videoElement = null; shakaContainer = null; currentVideoId = ''; sabrManifest = null; } function getPlayerInstance() { return player; } function getVideoElement() { return videoElement; } return { loadVideo: loadVideo, dispose: dispose, getPlayer: getPlayerInstance, getVideoElement: getVideoElement, initInnertube: initInnertube, fetchOnesieConfig: fetchOnesieConfig }; })(); window.SABRPlayer = SABRPlayer;