// ==UserScript== // @name [VOT] - Voice Over Translation // @name:de [VOT] - Voice-over-Übersetzung // @name:es [VOT] - Traducción por voz superpuesta // @name:fr [VOT] - Traduction en voix off // @name:it [VOT] - Traduzione fuori campo // @name:ru [VOT] - Закадровый перевод видео // @name:zh [VOT] - 配音翻译 // @namespace vot // @version 1.11.17 // @author Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng // @description Watch videos in other languages with voice-over translation and subtitles in any browser // @description:de Sieh dir Videos in anderen Sprachen mit Voice-over-Übersetzung und Untertiteln in jedem Browser an // @description:es Ve videos en otros idiomas con voice-over y subtítulos en cualquier navegador // @description:fr Regardez des vidéos dans d'autres langues avec une traduction en voix off et des sous-titres dans n'importe quel navigateur // @description:it Guarda video in altre lingue con doppiaggio fuori campo e sottotitoli in qualsiasi browser // @description:ru Смотрите видео на другом языке с закадровым переводом и субтитрами в любом браузере // @description:zh 在任何浏览器中观看其他语言的视频,享受配音翻译和字幕 // @license MIT // @icon https://translate.yandex.ru/icons/favicon.ico // @homepageURL https://github.com/ilyhalight/voice-over-translation // @source https://github.com/ilyhalight/voice-over-translation.git // @supportURL https://github.com/ilyhalight/voice-over-translation/issues // @downloadURL https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist/vot.user.js // @updateURL https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist/vot.user.js // @match *://*.youtube.com/* // @match *://*.youtube-nocookie.com/* // @match *://*.youtubekids.com/* // @match *://*.twitch.tv/* // @match *://*.xvideos.com/* // @match *://*.xvideos-ar.com/* // @match *://*.xvideos005.com/* // @match *://*.xv-ru.com/* // @match *://*.xhamster.com/* // @match *://*.xhamster.desi/* // @match *://*.xhvid.com/* // @match *://*.spankbang.com/* // @match *://*.rule34video.com/* // @match *://*.picarto.tv/* // @match *://*.olympics.com/* // @match *://*.pornhub.com/* // @match *://*.pornhub.org/* // @match *://*.vk.com/* // @match *://*.vkvideo.ru/* // @match *://*.vk.ru/* // @match *://*.vimeo.com/* // @match *://*.imdb.com/* // @match *://*.9gag.com/* // @match *://*.twitter.com/* // @match *://*.x.com/* // @match *://*.facebook.com/* // @match *://*.rutube.ru/* // @match *://*.bilibili.com/* // @match *://*.bilibili.tv/* // @match *://my.mail.ru/* // @match *://*.bitchute.com/* // @match *://*.coursera.org/* // @match *://*.udemy.com/course/* // @match *://*.tiktok.com/* // @match *://*.douyin.com/* // @match *://rumble.com/* // @match *://*.eporner.com/* // @match *://*.dailymotion.com/* // @match *://*.ok.ru/* // @match *://drive.google.com/file/d/*/view* // @match *://drive.google.com/drive/folders/* // @match *://trovo.live/* // @match *://disk.yandex.ru/* // @match *://disk.yandex.kz/* // @match *://disk.yandex.com/* // @match *://disk.yandex.com.am/* // @match *://disk.yandex.com.ge/* // @match *://disk.yandex.com.tr/* // @match *://disk.yandex.by/* // @match *://disk.yandex.az/* // @match *://disk.yandex.co.il/* // @match *://disk.yandex.ee/* // @match *://disk.yandex.lt/* // @match *://disk.yandex.lv/* // @match *://disk.yandex.md/* // @match *://disk.yandex.net/* // @match *://disk.yandex.tj/* // @match *://disk.yandex.tm/* // @match *://disk.yandex.uz/* // @match *://disk.360.yandex.ru/* // @match *://youtube.googleapis.com/embed/* // @match *://*.banned.video/* // @match *://*.madmaxworld.tv/* // @match *://*.weverse.io/* // @match *://*.newgrounds.com/* // @match *://*.egghead.io/* // @match *://*.youku.com/* // @match *://*.archive.org/* // @match *://*.patreon.com/* // @match *://*.reddit.com/* // @match *://*.kodikplayer.com/* // @match *://*.kick.com/* // @match *://developer.apple.com/* // @match *://dev.epicgames.com/* // @match *://*.rapid-cloud.co/* // @match *://odysee.com/* // @match *://learning.sap.com/* // @match *://*.watchporn.to/* // @match *://*.linkedin.com/* // @match *://*.porntn.com/* // @match *://*.dzen.ru/* // @match *://*.cloudflarestream.com/* // @match *://*.loom.com/* // @match *://*.artstation.com/learning/* // @match *://*.rt.com/* // @match *://*.bitview.net/* // @match *://*.kickstarter.com/* // @match *://*.thisvid.com/* // @match *://*.ign.com/* // @match *://*.bunkr.site/* // @match *://*.bunkr.black/* // @match *://*.bunkr.cat/* // @match *://*.bunkr.media/* // @match *://*.bunkr.red/* // @match *://*.bunkr.ws/* // @match *://*.bunkr.org/* // @match *://*.bunkr.sk/* // @match *://*.bunkr.si/* // @match *://*.bunkr.su/* // @match *://*.bunkr.ci/* // @match *://*.bunkr.cr/* // @match *://*.bunkr.fi/* // @match *://*.bunkr.ph/* // @match *://*.bunkr.pk/* // @match *://*.bunkr.ps/* // @match *://*.bunkr.ru/* // @match *://*.bunkr.la/* // @match *://*.bunkr.is/* // @match *://*.bunkr.to/* // @match *://*.bunkr.ac/* // @match *://*.bunkr.ax/* // @match *://web.telegram.org/k/* // @match *://rust-server-531j.onrender.com/* // @match *://mylearn.oracle.com/* // @match *://learn.deeplearning.ai/* // @match *://learn-staging.deeplearning.ai/* // @match *://learn-dev.deeplearning.ai/* // @match *://*.netacad.com/content/i2cs/* // @match *://*.netacad.com/authoring-resources/* // @match *://*.nicovideo.jp/* // @match *://*.zdf.de/* // @match *://iframe.mediadelivery.net/* // @match *://video.bunnycdn.com/* // @match *://*.weibo.com/* // @match *://*.jove.com/* // @match *://*.preservetube.com/* // @match *://*.mediafile.cc/* // @match *://*.skilljar.com/* // @match *://projector.datacamp.com/* // @match *://hot.noodlemagazine.com/* // @match *://fast.wistia.net/* // @match *://*.the-joi-database.com/* // @match *://*.vhx.tv/* // @match *://*/*.mp4* // @match *://*/*.webm* // @match *://*.yewtu.be/* // @match *://inv.nadeko.net/* // @match *://invidious.nerdvpn.de/* // @match *://invidious.protokolla.fi/* // @match *://invidious.materialio.us/* // @match *://iv.melmac.space/* // @match *://*.piped.video/* // @match *://piped.kavin.rocks/* // @match *://piped.private.coffee/* // @match *://proxitok.pabloferreiro.es/* // @match *://proxitok.pussthecat.org/* // @match *://tok.habedieeh.re/* // @match *://proxitok.esmailelbob.xyz/* // @match *://proxitok.privacydev.net/* // @match *://tok.artemislena.eu/* // @match *://tok.adminforge.de/* // @match *://tt.vern.cc/* // @match *://cringe.whatever.social/* // @match *://proxitok.lunar.icu/* // @match *://proxitok.privacy.com.de/* // @match *://peertube.tmp.rcp.tf/* // @match *://*.dalek.zone/* // @match *://video.sadmin.io/* // @match *://videos.viorsan.com/* // @match *://peertube.1312.media/* // @match *://tube.shanti.cafe/* // @match *://*.bee-tube.fr/* // @match *://video.blender.org/* // @match *://*.beetoons.tv/* // @match *://*.makertube.net/* // @match *://*.peertube.tv/* // @match *://*.framatube.org/* // @match *://*.tilvids.com/* // @match *://*.diode.zone/* // @match *://*.fedimovie.com/* // @match *://video.hardlimit.com/* // @match *://*.share.tube/* // @match *://*.peervideo.club/* // @match *://*.coursehunter.net/* // @match *://*.coursetrain.net/* // @exclude file://*/*.mp4* // @exclude file://*/*.webm* // @exclude *://accounts.youtube.com/* // @require https://gist.githubusercontent.com/ilyhalight/6eb5bb4dffc7ca9e3c57d6933e2452f3/raw/7ab38af2228d0bed13912e503bc8a9ee4b11828d/gm-addstyle-polyfill.js // @connect yandex.ru // @connect disk.yandex.kz // @connect disk.yandex.com // @connect disk.yandex.com.am // @connect disk.yandex.com.ge // @connect disk.yandex.com.tr // @connect disk.yandex.by // @connect disk.yandex.az // @connect disk.yandex.co.il // @connect disk.yandex.ee // @connect disk.yandex.lt // @connect disk.yandex.lv // @connect disk.yandex.md // @connect disk.yandex.net // @connect disk.yandex.tj // @connect disk.yandex.tm // @connect disk.yandex.uz // @connect disk.360.yandex.ru // @connect yandex.net // @connect timeweb.cloud // @connect raw.githubusercontent.com // @connect vimeo.com // @connect vhx.tv // @connect toil.cc // @connect onrender.com // @connect workers.dev // @connect eu.cc // @connect cloudflare-dns.com // @connect porntn.com // @connect youtube.com // @connect googlevideo.com // @connect vtrans.eu.cc // @connect vot-worker.vtrans.eu.cc // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_getValue // @grant GM_info // @grant GM_listValues // @grant GM_notification // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM.deleteValue // @grant GM.getValue // @grant GM.getValues // @grant GM.listValues // @grant GM.notification // @grant GM.setValue // @grant GM.xmlHttpRequest // @grant window.focus // ==/UserScript== var vot = (function(exports) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esmMin = (fn, res, err) => () => { if (err) throw err[0]; try { return fn && (res = fn(fn = 0)), res; } catch (e) { throw err = [e], e; } }; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion //#region node_modules/@vot.js/core/dist/types/service.js var VideoService$1; (function(VideoService) { VideoService["custom"] = "custom"; VideoService["directlink"] = "custom"; VideoService["youtube"] = "youtube"; VideoService["preservetube"] = "preservetube"; VideoService["piped"] = "piped"; VideoService["invidious"] = "invidious"; VideoService["niconico"] = "niconico"; VideoService["vk"] = "vk"; VideoService["nine_gag"] = "nine_gag"; VideoService["gag"] = "nine_gag"; VideoService["twitch"] = "twitch"; VideoService["proxitok"] = "proxitok"; VideoService["tiktok"] = "tiktok"; VideoService["vimeo"] = "vimeo"; VideoService["xvideos"] = "xvideos"; VideoService["xhamster"] = "xhamster"; VideoService["spankbang"] = "spankbang"; VideoService["rule34video"] = "rule34video"; VideoService["picarto"] = "picarto"; VideoService["olympicsreplay"] = "olympics_replay"; VideoService["pornhub"] = "pornhub"; VideoService["twitter"] = "twitter"; VideoService["x"] = "twitter"; VideoService["rumble"] = "rumble"; VideoService["facebook"] = "facebook"; VideoService["rutube"] = "rutube"; VideoService["coub"] = "coub"; VideoService["bilibili"] = "bilibili"; VideoService["mail_ru"] = "mailru"; VideoService["mailru"] = "mailru"; VideoService["bitchute"] = "bitchute"; VideoService["eporner"] = "eporner"; VideoService["peertube"] = "peertube"; VideoService["dailymotion"] = "dailymotion"; VideoService["trovo"] = "trovo"; VideoService["yandexdisk"] = "yandexdisk"; VideoService["ok_ru"] = "okru"; VideoService["okru"] = "okru"; VideoService["googledrive"] = "googledrive"; VideoService["bannedvideo"] = "bannedvideo"; VideoService["weverse"] = "weverse"; VideoService["weibo"] = "weibo"; VideoService["newgrounds"] = "newgrounds"; VideoService["egghead"] = "egghead"; VideoService["youku"] = "youku"; VideoService["archive"] = "archive"; VideoService["kodik"] = "kodik"; VideoService["patreon"] = "patreon"; VideoService["reddit"] = "reddit"; VideoService["kick"] = "kick"; VideoService["apple_developer"] = "apple_developer"; VideoService["appledeveloper"] = "apple_developer"; VideoService["epicgames"] = "epicgames"; VideoService["odysee"] = "odysee"; VideoService["coursehunterLike"] = "coursehunterLike"; VideoService["sap"] = "sap"; VideoService["watchpornto"] = "watchpornto"; VideoService["jove"] = "jove"; VideoService["linkedin"] = "linkedin"; VideoService["porntn"] = "porntn"; VideoService["dzen"] = "dzen"; VideoService["bunnystream"] = "bunnystream"; VideoService["cloudflarestream"] = "cloudflarestream"; VideoService["loom"] = "loom"; VideoService["rtnews"] = "rtnews"; VideoService["bitview"] = "bitview"; VideoService["thisvid"] = "thisvid"; VideoService["joidatabase"] = "joidatabase"; VideoService["ign"] = "ign"; VideoService["noodlemagazine"] = "noodlemagazine"; VideoService["zdf"] = "zdf"; VideoService["bunkr"] = "bunkr"; VideoService["imdb"] = "imdb"; VideoService["telegram"] = "telegram"; VideoService["wistia"] = "wistia"; })(VideoService$1 || (VideoService$1 = {})); //#endregion //#region node_modules/@vot.js/core/dist/utils/videoData.js var VideoDataError = class extends Error { constructor(message) { super(message); this.name = "VideoDataError"; } }; var localLinkRe = /(file:\/\/(\/)?|(http(s)?:\/\/)(127\.0\.0\.1|localhost|192\.168\.(\d){1,3}\.(\d){1,3}))/; var isCustomLink = (url) => { return !!/\.(m3u8|m4(a|v)|mpd)/.exec(url); }; //#endregion //#region node_modules/@vot.js/shared/dist/data/alternativeUrls.js var sitesInvidious = [ "yewtu.be", "inv.nadeko.net", "invidious.nerdvpn.de", "invidious.protokolla.fi", "invidious.materialio.us", "iv.melmac.space" ]; var sitesPiped = [ "piped.video", "piped.kavin.rocks", "piped.private.coffee" ]; var sitesProxiTok = [ "proxitok.pabloferreiro.es", "proxitok.pussthecat.org", "tok.habedieeh.re", "proxitok.esmailelbob.xyz", "proxitok.privacydev.net", "tok.artemislena.eu", "tok.adminforge.de", "tt.vern.cc", "cringe.whatever.social", "proxitok.lunar.icu", "proxitok.privacy.com.de" ]; var sitesPeertube = [ "peertube.tmp.rcp.tf", "dalek.zone", "video.sadmin.io", "videos.viorsan.com", "peertube.1312.media", "tube.shanti.cafe", "bee-tube.fr", "video.blender.org", "beetoons.tv", "makertube.net", "peertube.tv", "framatube.org", "tilvids.com", "diode.zone", "fedimovie.com", "video.hardlimit.com", "share.tube", "peervideo.club" ]; var sitesCoursehunterLike = ["coursehunter.net", "coursetrain.net"]; //#endregion //#region node_modules/@vot.js/ext/dist/types/service.js /** * Additional video services supported in extension */ var ExtVideoService; (function(ExtVideoService) { ExtVideoService["udemy"] = "udemy"; ExtVideoService["coursera"] = "coursera"; ExtVideoService["douyin"] = "douyin"; ExtVideoService["artstation"] = "artstation"; ExtVideoService["kickstarter"] = "kickstarter"; ExtVideoService["datacamp"] = "datacamp"; ExtVideoService["oraclelearn"] = "oraclelearn"; ExtVideoService["deeplearningai"] = "deeplearningai"; ExtVideoService["netacad"] = "netacad"; ExtVideoService["mediafile"] = "mediafile"; ExtVideoService["skilljar"] = "skilljar"; ExtVideoService["dropout"] = "dropout"; })(ExtVideoService || (ExtVideoService = {})); ({ ...VideoService$1, ...ExtVideoService }); //#endregion //#region node_modules/@vot.js/ext/dist/data/sites.js var sharedSelectors = { bilibiliPlayer: ".bpx-player-video-wrap, div.player-mobile-box.player-mobile-autoplay", flowplayer: ".fp-player, div.flowplayer", idPlayer: "#player", jwPlayer: ".jwplayer, .jw-media", player: ".player", shakaPlayer: ".shaka-video-container, [id^=\"shaka-video-container-\"]", videoJsUniversal: "[id^='vjs_video_']:not([id*='_html5_api']):not(video), video-js:not([id*='_html5_api']), .video-js:not(video):not([id*='_html5_api']), .vjs-player:not([id*='_html5_api']), [data-vjs-player]:not([id*='_html5_api'])", vkVideoPlayer: ".videoplayer_media, vk-video-player" }; var sites_default = [ { additionalData: "mobile", host: VideoService$1.youtube, url: "https://youtu.be/", match: /^m.youtube.com$/, selector: ".player-container", needExtraData: true }, { host: VideoService$1.youtube, url: "https://youtu.be/", match: (enteredUrl) => /^(www.)?youtube(-nocookie|kids)?.com$/.test(enteredUrl.hostname) && enteredUrl.pathname.startsWith("/tv"), selector: "#container", needExtraData: true }, { host: VideoService$1.youtube, url: "https://youtu.be/", match: (url) => /^(www.)?youtube(-nocookie|kids)?.com$/.test(url.host) && !url.pathname.startsWith("/embed/"), selector: ".html5-video-container:not(#inline-player *)", needExtraData: true }, { host: VideoService$1.youtube, url: "https://youtu.be/", additionalData: "embed", match: (url) => /^(www.)?youtube(-nocookie|kids)?.com$/.test(url.host) && url.pathname.startsWith("/embed/"), selector: "html", needExtraData: true }, { host: VideoService$1.youtube, url: "https://youtu.be/", match: (url) => /^music\.youtube\.com$/.test(url.host), selector: "#song-video", eventSelector: "#player", needExtraData: true }, { host: VideoService$1.invidious, url: "https://youtu.be/", match: sitesInvidious, selector: sharedSelectors.idPlayer, needBypassCSP: true }, { host: VideoService$1.piped, url: "https://youtu.be/", match: sitesPiped, selector: sharedSelectors.shakaPlayer, needBypassCSP: true }, { host: VideoService$1.preservetube, url: "https://preservetube.com/", match: /^preservetube\.com$/, selector: "div.video-wrapper", needExtraData: true }, { host: VideoService$1.zdf, url: "https://www.zdf.de/play/", match: [/^zdf.de$/, /^(www.)?zdf.de$/], selector: "div.zdfplayer-app.zdfplayer-desktop, div.zdfplayer-app" }, { host: VideoService$1.niconico, url: "https://www.nicovideo.jp/watch/", match: [/^(www\.|sp\.)?nicovideo\.jp$/, /^nico\.ms$/], selector: `[class*="grid-area_[player]"] > div` }, { additionalData: "mobile", host: VideoService$1.vk, url: "https://vk.com/", match: [/^m.vk.(com|ru)$/, /^m.vkvideo.ru$/], selector: sharedSelectors.vkVideoPlayer, shadowRoot: true, needExtraData: true }, { additionalData: "clips", host: VideoService$1.vk, url: "https://vk.com/", match: /^(www.|m.)?vk.(com|ru)$/, selector: "div[data-testid=\"clipcontainer-video\"]", needExtraData: true }, { host: VideoService$1.vk, url: "https://vk.com/", match: [/^(www\.|m\.)?vk\.(com|ru)$/, /^(.*\.)?vkvideo\.ru$/], selector: sharedSelectors.vkVideoPlayer, needExtraData: true }, { host: VideoService$1.nine_gag, url: "https://9gag.com/gag/", match: /^9gag.com$/, selector: ".video-post", needExtraData: true }, { host: VideoService$1.twitch, url: "https://twitch.tv/", match: [ /^m.twitch.tv$/, /^(www.)?twitch.tv$/, /^clips.twitch.tv$/, /^player.twitch.tv$/ ], needExtraData: true, selector: ".video-ref, main > div > section > div > div > div" }, { host: VideoService$1.proxitok, url: "https://www.tiktok.com/", match: sitesProxiTok, selector: ".column.has-text-centered" }, { host: VideoService$1.tiktok, url: "https://www.tiktok.com/", match: /^(www.)?tiktok.com$/, selector: null }, { host: ExtVideoService.douyin, url: "https://www.douyin.com/", match: /^(www.)?douyin.com/, selector: ".xg-video-container", needExtraData: true, needBypassCSP: true }, { host: VideoService$1.vimeo, url: "https://vimeo.com/", match: /^(www\.|m\.)?vimeo.com$/, needExtraData: true, selector: sharedSelectors.player }, { host: VideoService$1.vimeo, url: "https://player.vimeo.com/", match: /^player.vimeo.com$/, additionalData: "embed", needExtraData: true, needBypassCSP: true, selector: sharedSelectors.player }, { host: VideoService$1.xvideos, url: "https://www.xvideos.com/", match: [ /^(www.)?xvideos(-ar)?.com$/, /^(www.)?xvideos(\d\d\d).com$/, /^(www.)?xv-ru.com$/ ], selector: "#hlsplayer", needBypassCSP: true }, { host: VideoService$1.xhamster, url: "https://xhamster.com/", match: (url) => /^(?:[^.]+\.)?(?:xhamster\.(?:com|desi)|xhamster\d+\.(?:com|desi)|xhvid\.com)$/.test(url.host) && /\/(?:videos\/[^/]+-[\dA-Za-z]+)\/?$/.test(url.pathname), selector: "#player-container" }, { host: VideoService$1.spankbang, url: "https://spankbang.com/", match: (url) => /^(?:[^.]+\.)?spankbang\.com$/.test(url.host) && /\/(?:[\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?|[\da-z]+-[\da-z]+\/playlist\/[^/?#&]+)\/?$/i.test(url.pathname), selector: "#main_video_player" }, { host: VideoService$1.rule34video, url: "https://rule34video.com/video/", match: (url) => /^(www\.)?rule34video\.com$/.test(url.host) && /\/videos?\/\d+/.test(url.pathname), selector: sharedSelectors.flowplayer }, { host: VideoService$1.joidatabase, url: "https://www.the-joi-database.com/api/stream/", match: [/^s1\.the-joi-database\.com$/, /^(www\.)?the-joi-database\.com$/], selector: ".plyr__video-wrapper", needExtraData: true }, { host: VideoService$1.picarto, url: "https://picarto.tv/", match: (url) => /^(www\.)?picarto\.tv$/.test(url.host) && /^(?:\/[^/]+\/(?:profile\/)?videos\/[^/?#&]+|\/videopopout\/[^/?#&]+|\/[^/#?]+\/?)$/.test(url.pathname), selector: `[class*="VideosTab__PlayerWrapper"]` }, { host: VideoService$1.olympicsreplay, url: "https://olympics.com/", match: (url) => /^(www\.)?olympics\.com$/.test(url.host) && /^\/[a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+\/?$/i.test(url.pathname), selector: sharedSelectors.videoJsUniversal }, { host: VideoService$1.pornhub, url: "https://rt.pornhub.com/view_video.php?viewkey=", match: /^[a-z]+.pornhub.(com|org)$/, selector: "div.video-element-wrapper-js" }, { additionalData: "embed", host: VideoService$1.pornhub, url: "https://rt.pornhub.com/view_video.php?viewkey=", match: (url) => /^[a-z]+.pornhub.(com|org)$/.exec(url.host) && url.pathname.startsWith("/embed/"), selector: sharedSelectors.idPlayer }, { host: VideoService$1.twitter, url: "https://twitter.com/i/status/", match: /^(twitter|x).com$/, selector: "div[data-testid=\"videoComponent\"]", needBypassCSP: true }, { host: VideoService$1.rumble, url: "https://rumble.com/", match: /^rumble.com$/, selector: `[id^="vid_"] > div` }, { host: VideoService$1.facebook, url: "https://facebook.com/", match: (url) => url.host.includes("facebook.com") && url.pathname.includes("/videos/"), selector: "div[role=\"main\"] div[data-pagelet$=\"video\" i]", needBypassCSP: true }, { additionalData: "reels", host: VideoService$1.facebook, url: "https://facebook.com/", match: (url) => url.host.includes("facebook.com") && url.pathname.includes("/reel/"), selector: "div[role=\"main\"]", needBypassCSP: true }, { host: VideoService$1.rutube, url: "https://rutube.ru/video/", match: /^rutube.ru$/, selector: `div[class*="videoWrapper"]` }, { additionalData: "embed", host: VideoService$1.rutube, url: "https://rutube.ru/video/", match: /^rutube.ru$/, selector: "#app > div > div" }, { host: VideoService$1.bilibili, url: "https://www.bilibili.com/", match: /^(www|m|player).bilibili.com$/, selector: sharedSelectors.bilibiliPlayer }, { host: VideoService$1.bilibili, url: "https://www.bilibili.tv/", match: /^(?:www\.|m\.)?bilibili\.tv$/, selector: sharedSelectors.bilibiliPlayer }, { additionalData: "old", host: VideoService$1.bilibili, url: "https://www.bilibili.com/", match: /^(www|m).bilibili.com$/, selector: null }, { host: VideoService$1.mailru, url: "https://my.mail.ru/", match: /^my.mail.ru$/, selector: "#b-video-wrapper" }, { host: VideoService$1.bitchute, url: "https://www.bitchute.com/video/", match: /^(www.)?bitchute.com$/, selector: sharedSelectors.videoJsUniversal }, { host: VideoService$1.eporner, url: "https://www.eporner.com/", match: /^(www.)?eporner.com$/, selector: sharedSelectors.videoJsUniversal }, { host: VideoService$1.peertube, url: "stub", match: sitesPeertube, selector: sharedSelectors.videoJsUniversal }, { host: VideoService$1.dailymotion, url: "https://www.dailymotion.com/video/", match: /^((www\.|player\.)?dailymotion\.com|geo(\d+)?\.dailymotion\.com|dai\.ly)$/, selector: sharedSelectors.player }, { host: VideoService$1.trovo, url: "https://trovo.live/s/", match: /^trovo.live$/, selector: ".player-video" }, { host: VideoService$1.yandexdisk, url: "https://yadi.sk/", match: /^disk.yandex.(ru|kz|com(\.(am|ge|tr))?|by|az|co\.il|ee|lt|lv|md|net|tj|tm|uz)$/, selector: ".video-player__player > div:nth-child(1)", needBypassCSP: true, needExtraData: true }, { host: VideoService$1.okru, url: "https://ok.ru/video/", match: /^ok.ru$/, selector: sharedSelectors.vkVideoPlayer, shadowRoot: true }, { host: VideoService$1.googledrive, url: "https://drive.google.com/file/d/", match: /^youtube.googleapis.com$/, selector: "html" }, { host: VideoService$1.bannedvideo, url: "https://madmaxworld.tv/watch?id=", match: /^(www.)?banned.video|madmaxworld.tv$/, selector: sharedSelectors.videoJsUniversal, needExtraData: true }, { host: VideoService$1.weverse, url: "https://weverse.io/", match: /^weverse.io$/, selector: ".webplayer-internal-source-wrapper", needExtraData: true }, { host: VideoService$1.weibo, url: "https://weibo.com/", match: (url) => /^(?:www\.)?weibo\.com$/.test(url.host) && /^\/(?:\d+\/[A-Za-z0-9]+|0\/[A-Za-z0-9]+|tv\/show\/\d+:(?:[\da-f]{32}|\d{16,}))\/?$/.test(url.pathname) || /^video\.weibo\.com$/.test(url.host) && /^\/show\/?$/.test(url.pathname) && /^\d+:(?:[\da-f]{32}|\d{16,})$/i.test(url.searchParams.get("fid") ?? "") || /^(?:www\.)?weibo\.com$/.test(url.host) && /^\/newlogin\/?$/.test(url.pathname) && (url.searchParams.has("url") || /^[A-Za-z0-9]+$/.test(url.searchParams.get("layerid") ?? "")), selector: sharedSelectors.videoJsUniversal || "#playVideo" }, { host: VideoService$1.newgrounds, url: "https://www.newgrounds.com/", match: /^(www.)?newgrounds.com$/, selector: ".ng-video-player" }, { host: VideoService$1.egghead, url: "https://egghead.io/", match: /^egghead.io$/, selector: ".cueplayer-react-video-holder" }, { host: VideoService$1.youku, url: "https://v.youku.com/", match: /^v.youku.com$/, selector: "#ykPlayer" }, { host: VideoService$1.archive, url: "https://archive.org/details/", match: /^archive.org$/, selector: sharedSelectors.jwPlayer, needExtraData: true }, { host: VideoService$1.kodik, url: "stub", match: /^kodikplayer.com$/, selector: sharedSelectors.flowplayer, needExtraData: true }, { host: VideoService$1.patreon, url: "stub", match: /^(www.)?patreon.com$/, selector: `[class*="videoArea"]`, needExtraData: true }, { additionalData: "old", host: VideoService$1.reddit, url: "stub", match: /^old.reddit.com$/, selector: ".reddit-video-player-root", needExtraData: true, needBypassCSP: true }, { host: VideoService$1.reddit, url: "stub", match: /^(www.|new.)?reddit.com$/, selector: "div[slot=post-media-container]", shadowRoot: true, needExtraData: true, needBypassCSP: true }, { host: VideoService$1.kick, url: "https://kick.com/", match: /^kick.com$/, selector: "#injected-embedded-channel-player-video > div", needExtraData: true }, { host: VideoService$1.appledeveloper, url: "https://developer.apple.com/", match: /^developer.apple.com$/, selector: ".developer-video-player", needExtraData: true, needBypassCSP: true }, { host: VideoService$1.epicgames, url: "https://dev.epicgames.com/community/learning/", match: /^dev.epicgames.com$/, selector: sharedSelectors.videoJsUniversal, needExtraData: true }, { host: VideoService$1.odysee, url: "stub", match: /^odysee.com$/, selector: ".video-js-parent", needExtraData: true }, { host: VideoService$1.coursehunterLike, url: "stub", match: sitesCoursehunterLike, selector: null, needExtraData: true }, { host: VideoService$1.sap, url: "https://learning.sap.com/courses/", match: /^learning.sap.com$/, selector: ".kaltura-player-container", needExtraData: true, needBypassCSP: true }, { host: ExtVideoService.udemy, url: "https://www.udemy.com/", match: /udemy.com$/, selector: sharedSelectors.shakaPlayer, needExtraData: true }, { host: ExtVideoService.datacamp, url: "https://www.datacamp.com/courses/", match: (url) => /^(?:campus\.|projector\.)?datacamp\.com$/.test(url.hostname), selector: sharedSelectors.videoJsUniversal, needExtraData: true }, { host: ExtVideoService.coursera, url: "https://www.coursera.org/", match: /coursera.org$/, selector: sharedSelectors.videoJsUniversal, needExtraData: true }, { host: VideoService$1.watchpornto, url: "https://watchporn.to/", match: /^watchporn.to$/, selector: sharedSelectors.flowplayer }, { host: VideoService$1.jove, url: "https://jove.com/", match: /^(?:app|www)\.jove\.com$/, selector: sharedSelectors.flowplayer, needExtraData: true }, { host: VideoService$1.linkedin, url: "https://www.linkedin.com/learning/", match: /^(www.)?linkedin.com$/, selector: sharedSelectors.videoJsUniversal, needExtraData: true, needBypassCSP: true }, { host: VideoService$1.porntn, url: "https://porntn.com/videos/", match: /^porntn.com$/, selector: sharedSelectors.flowplayer, needExtraData: true }, { host: VideoService$1.dzen, url: "https://dzen.ru/video/watch/", match: /^dzen.ru$/, selector: `[class*="player__playerWrap"] > div` }, { host: VideoService$1.bunnystream, url: "stub", match: [ /^video\.bunnycdn\.com$/, /^iframe\.mediadelivery\.net$/, /^(?:[^.]+\.)*b-cdn\.net$/ ], selector: null }, { host: VideoService$1.cloudflarestream, url: "stub", match: /^(watch|embed|iframe|customer-[^.]+).cloudflarestream.com$/, selector: null }, { host: VideoService$1.loom, url: "https://www.loom.com/share/", match: /^(www.)?loom.com$/, selector: ".VideoLayersContainer", needExtraData: true, needBypassCSP: true }, { host: ExtVideoService.artstation, url: "https://www.artstation.com/learning/", match: /^(www.)?artstation.com$/, selector: sharedSelectors.videoJsUniversal, needExtraData: true }, { host: VideoService$1.rtnews, url: "https://www.rt.com/", match: /^(www.)?rt.com$/, selector: sharedSelectors.jwPlayer, needExtraData: true }, { host: VideoService$1.noodlemagazine, url: "https://hot.noodlemagazine.com/", match: /^(hot\.)?noodlemagazine\.com$/, selector: sharedSelectors.jwPlayer, needExtraData: true }, { host: VideoService$1.bitview, url: "https://www.bitview.net/watch?v=", match: /^(www.)?bitview.net$/, selector: ".vlScreen", needExtraData: true }, { host: ExtVideoService.kickstarter, url: "https://www.kickstarter.com/", match: /^(www.)?kickstarter.com/, selector: ".ksr-video-player", needExtraData: true }, { host: VideoService$1.thisvid, url: "https://thisvid.com/", match: /^(www.)?thisvid.com$/, selector: sharedSelectors.flowplayer }, { additionalData: "regional", host: VideoService$1.ign, url: "https://de.ign.com/", match: /^(\w{2}.)?ign.com$/, needExtraData: true, selector: ".video-container" }, { host: VideoService$1.ign, url: "https://www.ign.com/", match: /^(www.)?ign.com$/, selector: sharedSelectors.player, needExtraData: true }, { host: VideoService$1.bunkr, url: "https://bunkr.site/", match: /^bunkr\.(site|black|cat|media|red|site|ws|org|s[kiu]|c[ir]|fi|p[hks]|ru|la|is|to|a[cx])$/, needExtraData: true, selector: ".plyr__video-wrapper" }, { host: VideoService$1.imdb, url: "https://www.imdb.com/video/", match: /^(www\.)?imdb\.com$/, selector: sharedSelectors.jwPlayer }, { host: VideoService$1.telegram, url: "https://t.me/", match: (url) => /^web\.telegram\.org$/.test(url.hostname) && url.pathname.startsWith("/k"), selector: ".media-viewer-aspecter, .ckin__player" }, { host: ExtVideoService.oraclelearn, url: "https://mylearn.oracle.com/ou/course/", match: /^mylearn\.oracle\.com/, selector: sharedSelectors.videoJsUniversal, needExtraData: true, needBypassCSP: true }, { host: ExtVideoService.deeplearningai, url: "https://learn.deeplearning.ai/courses/", match: /^learn(-dev|-staging)?\.deeplearning\.ai/, selector: ".lesson-video-player", needExtraData: true }, { host: ExtVideoService.netacad, url: "https://www.netacad.com/", match: /^(www\.)?netacad\.com/, selector: sharedSelectors.videoJsUniversal, shadowRoot: true, needExtraData: true }, { host: ExtVideoService.mediafile, url: "https://mediafile.cc/", match: /^(www\.)?mediafile\.cc$/, selector: "div#playerContainer", needExtraData: true }, { host: ExtVideoService.skilljar, url: "https://anthropic.skilljar.com/", match: /skilljar\.com$/, selector: sharedSelectors.jwPlayer, needExtraData: true }, { host: ExtVideoService.dropout, url: "https://embed.vhx.tv/videos/", match: /^embed\.vhx\.tv$/, selector: "body", needExtraData: true, needBypassCSP: true }, { host: VideoService$1.wistia, url: "https://fast.wistia.net/embed/iframe/", match: /^fast.wistia.net$/, selector: ".w-video-wrapper" }, { host: VideoService$1.custom, url: "stub", match: (url) => /([^/]+)\.(mp4|webm)/.test(url.pathname), rawResult: true } ]; //#endregion //#region node_modules/@vot.js/shared/dist/data/config.js var config_default$1 = { host: "api.browser.yandex.ru", hostWorker: "vot-worker.toil.cc", mediaProxy: "media-proxy.toil.cc", userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 YaBrowser/26.8.0.0 Safari/537.36", componentVersion: "26.8.5.811", chromiumRevision: "811", hmac: "bt8xH3VOlb4mqf0nqAibnDOoiPlXsisf", defaultDuration: 310, minChunkSize: 5295308, loggerLevel: 1, version: "3.1.5" }; //#endregion //#region node_modules/@vot.js/shared/dist/types/logger.js var LoggerLevel; (function(LoggerLevel) { LoggerLevel[LoggerLevel["DEBUG"] = 0] = "DEBUG"; LoggerLevel[LoggerLevel["INFO"] = 1] = "INFO"; LoggerLevel[LoggerLevel["WARN"] = 2] = "WARN"; LoggerLevel[LoggerLevel["ERROR"] = 3] = "ERROR"; LoggerLevel[LoggerLevel["SILENCE"] = 4] = "SILENCE"; })(LoggerLevel || (LoggerLevel = {})); //#endregion //#region node_modules/@vot.js/shared/dist/utils/logger.js var prefix = `[vot.js v${config_default$1.version}]`; function canLog(level) { return config_default$1.loggerLevel <= level; } function log$1(...messages) { if (!canLog(LoggerLevel.DEBUG)) return; console.log(prefix, ...messages); } function info(...messages) { if (!canLog(LoggerLevel.INFO)) return; console.info(prefix, ...messages); } function warn$1(...messages) { if (!canLog(LoggerLevel.WARN)) return; console.warn(prefix, ...messages); } function error$1(...messages) { if (!canLog(LoggerLevel.ERROR)) return; console.error(prefix, ...messages); } var Logger$1 = { canLog, log: log$1, info, warn: warn$1, error: error$1 }; //#endregion //#region node_modules/@vot.js/shared/dist/utils/utils.js /** * not all possible languages are listed here, but only those that are supported in voice-over-translation * src: https://gist.github.com/jackdoe/043297c8f739c53a6865138aa1dc3fe2 */ var iso6392to6391 = { afr: "af", aka: "ak", alb: "sq", amh: "am", ara: "ar", arm: "hy", asm: "as", aym: "ay", aze: "az", baq: "eu", bel: "be", ben: "bn", bos: "bs", bul: "bg", bur: "my", cat: "ca", chi: "zh", cos: "co", cze: "cs", dan: "da", div: "dv", dut: "nl", eng: "en", epo: "eo", est: "et", ewe: "ee", fin: "fi", fre: "fr", fry: "fy", geo: "ka", ger: "de", gla: "gd", gle: "ga", glg: "gl", gre: "el", grn: "gn", guj: "gu", hat: "ht", hau: "ha", hin: "hi", hrv: "hr", hun: "hu", ibo: "ig", ice: "is", ind: "id", ita: "it", jav: "jv", jpn: "ja", kan: "kn", kaz: "kk", khm: "km", kin: "rw", kir: "ky", kor: "ko", kur: "ku", lao: "lo", lat: "la", lav: "lv", lin: "ln", lit: "lt", ltz: "lb", lug: "lg", mac: "mk", mal: "ml", mao: "mi", mar: "mr", may: "ms", mlg: "mg", mlt: "mt", mon: "mn", nep: "ne", nor: "no", nya: "ny", ori: "or", orm: "om", pan: "pa", per: "fa", pol: "pl", por: "pt", pus: "ps", que: "qu", rum: "ro", rus: "ru", san: "sa", sin: "si", slo: "sk", slv: "sl", smo: "sm", sna: "sn", snd: "sd", som: "so", sot: "st", spa: "es", srp: "sr", sun: "su", swa: "sw", swe: "sv", tam: "ta", tat: "tt", tel: "te", tgk: "tg", tha: "th", tir: "ti", tso: "ts", tuk: "tk", tur: "tr", uig: "ug", ukr: "uk", urd: "ur", uzb: "uz", vie: "vi", wel: "cy", xho: "xh", yid: "yi", yor: "yo", zul: "zu" }; /** * Fetch wrapper with timeout support. * * Notes: * - Respects a user-provided AbortSignal and will abort if either the signal * or the timeout triggers. */ async function fetchWithTimeout(url, options = { headers: { "User-Agent": config_default$1.userAgent } }) { let { timeout = 3e3, signal, ...fetchOptions } = options; if (!signal && (!timeout || timeout <= 0)) return await fetch(url, fetchOptions); signal ??= AbortSignal.timeout(timeout); return await fetch(url, { ...fetchOptions, signal }); } /** * Get current timestamp in seconds */ function getTimestamp$1() { return Math.floor(Date.now() / 1e3); } /** * Convert lang to ISO 639-1 */ function normalizeLang$1(lang) { if (lang.length === 3) return iso6392to6391[lang]; return lang.toLowerCase().split(/[_;-]/)[0].trim(); } /** * Convert media .mp4/.webm link to proxied link with media proxy */ function proxyMedia(url, format = "mp4") { const generalUrl = `https://${config_default$1.mediaProxy}/v1/proxy/video.${format}?format=base64&force=true`; if (!(url instanceof URL)) return `${generalUrl}&url=${btoa(url)}`; return `${generalUrl}&url=${btoa(url.href)}&origin=${url.origin}&referer=${url.origin}`; } //#endregion //#region node_modules/@vot.js/ext/dist/helpers/base.js var VideoHelperError = class extends Error { constructor(message) { super(message); this.name = "VideoHelperError"; } }; var BaseHelper = class { API_ORIGIN = window.location.origin; fetch; extraInfo; referer; origin; service; video; language; constructor({ fetchFn = fetchWithTimeout, extraInfo = true, referer = document.referrer ?? `${window.location.origin}/`, origin = window.location.origin, service, video, language = "en" } = {}) { this.fetch = fetchFn; this.extraInfo = extraInfo; this.referer = referer; this.origin = /^(http(s)?):\/\//.test(String(origin)) ? origin : window.location.origin; this.service = service; this.video = video; this.language = language; } getVideoData(_videoId) { return Promise.resolve(void 0); } getVideoId(_url) { return Promise.resolve(void 0); } returnBaseData(videoId) { if (!this.service) return; return { url: this.service.url + videoId, videoId, host: this.service.host, duration: void 0 }; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/appledeveloper.js var AppleDeveloperHelper = class extends BaseHelper { API_ORIGIN = "https://developer.apple.com"; async getVideoData(videoId) { try { const contentUrl = document.querySelector("meta[property='og:video']")?.content; if (!contentUrl) throw new VideoHelperError("Failed to find content url"); return { url: contentUrl }; } catch (err) { Logger$1.error(`Failed to get apple developer video data by video ID: ${videoId}`, err.message); return; } } async getVideoId(url) { return /videos\/play\/([^/]+)\/([\d]+)/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/archive.js var ArchiveHelper = class extends BaseHelper { async getVideoId(url) { const videoId = /(details|embed)\/(.+)/.exec(url.pathname)?.[2]; if (!videoId) return void 0; return videoId.replace(/\/$/, "") || void 0; } async getVideoData(videoId) { if (!videoId) return void 0; const downloadUrl = `https://archive.org/download/${videoId.replace(/\+/g, "%20").replace(/\.[^.]+$/, ".mp4")}`; return { url: videoId, video_url: downloadUrl, translationHelp: [{ target: "video_file_url", targetUrl: downloadUrl }] }; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/artstation.js var ArtstationHelper = class extends BaseHelper { API_ORIGIN = "https://www.artstation.com/api/v2/learning"; getCSRFToken() { return document.querySelector("meta[name=\"public-csrf-token\"]")?.content; } async getCourseInfo(courseId) { try { const csrfToken = this.getCSRFToken(); return await (await this.fetch(`${this.API_ORIGIN}/courses/${courseId}/autoplay.json`, { method: "POST", headers: csrfToken ? { "PUBLIC-CSRF-TOKEN": csrfToken } : {} })).json(); } catch (err) { Logger$1.error(`Failed to get artstation course info by courseId: ${courseId}.`, err.message); return false; } } async getVideoUrl(chapterId) { try { return (await (await this.fetch(`${this.API_ORIGIN}/quicksilver/video_url.json?chapter_id=${chapterId}`)).json()).url.replace("qsep://", "https://"); } catch (err) { Logger$1.error(`Failed to get artstation video url by chapterId: ${chapterId}.`, err.message); return false; } } async getVideoData(videoId) { const [, courseId, , , chapterId] = videoId.split("/"); const courseInfo = await this.getCourseInfo(courseId); if (!courseInfo) return; const chapter = courseInfo.chapters.find((chapter) => chapter.hash_id === chapterId); if (!chapter) return; const videoUrl = await this.getVideoUrl(chapter.id); if (!videoUrl) return; const { title, duration, subtitles: videoSubtitles } = chapter; return { url: videoUrl, title, duration, subtitles: videoSubtitles.filter((subtitle) => subtitle.format === "vtt").map((subtitle) => ({ language: normalizeLang$1(subtitle.locale), source: "artstation", format: "vtt", url: subtitle.file_url })) }; } async getVideoId(url) { return /courses\/(\w{3,5})\/([^/]+)\/chapters\/(\w{3,5})/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/bannedvideo.js var BannedVideoHelper = class extends BaseHelper { API_ORIGIN = "https://api.banned.video"; async getVideoInfo(videoId) { try { return await (await this.fetch(`${this.API_ORIGIN}/graphql`, { method: "POST", body: JSON.stringify({ operationName: "GetVideo", query: `query GetVideo($id: String!) { getVideo(id: $id) { title description: summary duration: videoDuration videoUrl: directUrl isStream: live } }`, variables: { id: videoId } }), headers: { "User-Agent": "bannedVideoFrontEnd", "apollographql-client-name": "banned-web", "apollographql-client-version": "1.3", "content-type": "application/json" } })).json(); } catch (err) { console.error(`Failed to get bannedvideo video info by videoId: ${videoId}.`, err.message); return false; } } async getVideoData(videoId) { const videoInfo = await this.getVideoInfo(videoId); if (!videoInfo) return; const { videoUrl, duration, isStream, description, title } = videoInfo.data.getVideo; return { url: videoUrl, duration, isStream, title, description }; } async getVideoId(url) { return url.searchParams.get("id") ?? void 0; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/bilibili.js var BilibiliHelper = class extends BaseHelper { async getVideoId(url) { const playId = /(?:bangumi|cheese)\/play\/[^/]+/.exec(url.pathname)?.[0]; if (playId) return playId; const bvid = url.searchParams.get("bvid"); if (bvid) return `video/${bvid}`; const intlId = /^\/(?:[a-z]{2}\/)?((?:play\/\d+(?:\/\d+)?|video\/\d+))\/?$/i.exec(url.pathname)?.[1]; if (intlId) return intlId; const vid = /video\/[^/]+/.exec(url.pathname)?.[0]; if (vid) { const p = url.searchParams.get("p"); return p !== null ? `${vid}/?p=${p}` : vid; } } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/bitchute.js var BitchuteHelper = class extends BaseHelper { async getVideoId(url) { return /(video|embed)\/([^/]+)/.exec(url.pathname)?.[2]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/bitview.js var BitviewHelper = class extends BaseHelper { async getVideoData(videoId) { try { const videoUrl = document.querySelector(".vlScreen > video")?.src; if (!videoUrl) throw new VideoHelperError("Failed to find video URL"); return { url: videoUrl }; } catch (err) { Logger$1.error(`Failed to get Bitview data by videoId: ${videoId}`, err.message); return; } } async getVideoId(url) { return url.searchParams.get("v"); } }; //#endregion //#region node_modules/@vot.js/ext/dist/players/plyr.js var PlyrHelper = class { SUBTITLE_SOURCE = "plyr"; SUBTITLE_FORMAT = "vtt"; getPlayer() { const customWindow = window; if (customWindow.player?.media) return customWindow.player; return document.querySelector("video, audio, .plyr video")?.plyr || void 0; } getVideoData(videoId) { try { const player = this.getPlayer(); const videoEl = document.querySelector("video, audio, .plyr video"); if (!player && !videoEl) throw new Error("Plyr player or media element not found"); const duration = player?.duration ?? videoEl?.duration ?? 0; const fileUrl = player?.source || videoEl?.currentSrc || videoEl?.src || videoEl?.querySelector("source")?.getAttribute("src"); if (!fileUrl) throw new Error("Failed to find video url"); return { url: videoId, duration, translationHelp: [{ target: "video_file_url", targetUrl: fileUrl }] }; } catch (err) { console.error("[VOT] PlyrHelper error:", err instanceof Error ? err.message : String(err)); return; } } getSubtitles() { const subtitles = []; try { const player = this.getPlayer(); const videoEl = document.querySelector("video, audio, .plyr video"); const rawTracks = []; const searchTargets = []; if (videoEl) searchTargets.push(videoEl); if (player?.elements?.container) searchTargets.push(player.elements.container); for (const target of searchTargets) { const tracks = Array.from(target.querySelectorAll("track[src]")); for (const track of tracks) { const src = track.getAttribute("src"); if (src && track.kind !== "metadata") rawTracks.push({ src, lang: track.srclang || "" }); } } if (player?.config?.tracks && Array.isArray(player.config.tracks)) { const configTracks = player.config.tracks; for (const track of configTracks) if (track?.src && track.kind !== "metadata") rawTracks.push({ src: track.src, lang: track.srclang || track.lang || "" }); } const seenUrls = /* @__PURE__ */ new Set(); for (const { src, lang } of rawTracks) try { const absUrl = new URL(src, window.location.href).toString(); if (!seenUrls.has(absUrl)) { seenUrls.add(absUrl); subtitles.push({ source: this.SUBTITLE_SOURCE, format: this.SUBTITLE_FORMAT, language: normalizeLang$1(lang), url: absUrl }); } } catch {} } catch (err) { console.error("[VOT] PlyrHelper getSubtitles error:", err); } return subtitles; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/bunkr.js var BunkrHelper = class extends BaseHelper { async getVideoData(videoId) { return new PlyrHelper().getVideoData(videoId); } async getVideoId(url) { return /\/(?:f|v)\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/bunnystream.js var BunnyStreamHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname + url.search; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/cloudflarestream.js var CloudflareStreamHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname + url.search; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/coursehunterLike.js var CoursehunterLikeHelper = class extends BaseHelper { API_ORIGIN = this.origin ?? "https://coursehunter.net"; async getCourseId() { const courseId = window.course_id; if (courseId !== void 0) return String(courseId); return document.querySelector("input[name=\"course_id\"]")?.value; } async getLessonsData(courseId) { const lessons = window.lessons; if (lessons?.length) return lessons; try { return await (await this.fetch(`${this.API_ORIGIN}/api/v1/course/${courseId}/lessons`)).json(); } catch (err) { Logger$1.error(`Failed to get CoursehunterLike lessons data by courseId: ${courseId}, because ${err.message}`); return; } } getLessondId(videoId) { let lessondId = videoId.split("?lesson=")?.[1]; if (lessondId) return +lessondId; lessondId = document.querySelector(".lessons-item_active")?.dataset?.index; if (lessondId) return +lessondId; return 1; } async getVideoData(videoId) { const courseId = await this.getCourseId(); if (!courseId) return; const lessonsData = await this.getLessonsData(courseId); if (!lessonsData) return; const lessonId = this.getLessondId(videoId); const { file: videoUrl, duration, title } = lessonsData?.[lessonId - 1]; if (!videoUrl) return; return { url: videoId, video_url: videoUrl, translationHelp: [{ target: "video_file_url", targetUrl: proxyMedia(videoUrl) }], duration, title }; } async getVideoId(url) { return url.href; } }; //#endregion //#region node_modules/@vot.js/shared/dist/data/consts.js var availableLangs = [ "auto", "ru", "en", "zh", "ko", "ar", "fr", "it", "es", "de", "ja" ]; var availableTTS = [ "ru", "en", "kk" ]; //#endregion //#region node_modules/@vot.js/ext/dist/utils/dom.js /** * querySelector that also pierces open shadow roots at any depth. */ function querySelectorDeep(selector, root = document) { const match = root.querySelector(selector); if (match) return match; for (const el of root.querySelectorAll("*")) if (el.shadowRoot) { const nested = querySelectorDeep(selector, el.shadowRoot); if (nested) return nested; } return null; } //#endregion //#region node_modules/@vot.js/ext/dist/players/videojs.js function isPreferredMedia(source) { const type = source.type?.toLowerCase().split(";")[0].trim(); if (type === "video/mp4" || type === "video/webm") return true; const path = source.src?.split(/[?#]/)[0].toLowerCase(); return Boolean(path?.endsWith(".mp4") || path?.endsWith(".webm")); } function selectVideoUrl(candidates) { const available = candidates.filter((source) => Boolean(source.src)); return (available.find(isPreferredMedia) ?? available[0])?.src; } /** * Shared class for all videojs players */ var VideoJSHelper = class VideoJSHelper extends BaseHelper { SUBTITLE_SOURCE = "videojs"; SUBTITLE_FORMAT = "vtt"; static VIDEOJS_SELECTOR = "video.vjs-tech, video[id$='_html5_api'], video[src], video"; static getTechEl(isShadowRoot = false) { return isShadowRoot ? querySelectorDeep(VideoJSHelper.VIDEOJS_SELECTOR) : document.querySelector(VideoJSHelper.VIDEOJS_SELECTOR); } static getPlayer(isShadowRoot = false) { const vjs = window.videojs; const techEl = VideoJSHelper.getTechEl(isShadowRoot); const derivedPlayerId = techEl?.id?.endsWith("_html5_api") ? techEl.id.slice(0, -10) : void 0; if (vjs?.getPlayer) { if (derivedPlayerId) { const p = vjs.getPlayer(derivedPlayerId); if (p) return p; } if (techEl) { const p = vjs.getPlayer(techEl); if (p) return p; } } const players = (typeof vjs?.getPlayers === "function" ? vjs.getPlayers() : vjs?.players) ?? {}; for (const p of Object.values(players)) { const player = p; const innerVideo = (typeof player.el === "function" ? player.el() : null)?.querySelector?.("video.vjs-tech, video") ?? null; if (innerVideo && techEl && innerVideo === techEl) return p; if (derivedPlayerId && typeof player.id === "function" && player.id() === derivedPlayerId) return p; } } getVideoDataByPlayer(videoId, isShadowRoot = false) { try { const player = VideoJSHelper.getPlayer(isShadowRoot); const techEl = VideoJSHelper.getTechEl(isShadowRoot); if (!player && !techEl) throw new Error(`Video player/video element not found, videoId ${videoId}`); const duration = player?.duration?.() ?? techEl?.duration; const candidates = []; if (player) { const sources = typeof player.currentSources === "function" ? player.currentSources() : player.getCache?.()?.sources; if (Array.isArray(sources)) candidates.push(...sources); } if (techEl) candidates.push({ src: techEl.currentSrc }, { src: techEl.src }, ...Array.from(techEl.querySelectorAll("source")).map((source) => ({ src: source.src, type: source.getAttribute("type") })), { src: techEl.getAttribute?.("src") }); const url = selectVideoUrl(candidates); if (!url) throw new Error(`Failed to find video url for videoID ${videoId}`); return { url, duration, subtitles: this.getSubtitles() }; } catch (err) { Logger$1.error("Failed to get videojs video data", err.message); return; } } getSubtitles() { const techEl = VideoJSHelper.getTechEl(); return (techEl ? Array.from(techEl.querySelectorAll("track[src]")) : []).filter((t) => t.kind !== "metadata").flatMap((t) => { const src = t.getAttribute("src"); if (!src) return []; const absUrl = new URL(src, window.location.href).toString(); return [{ language: normalizeLang$1(t.srclang || ""), source: this.SUBTITLE_SOURCE, format: this.SUBTITLE_FORMAT, url: absUrl }]; }); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/coursera.js var CourseraHelper = class CourseraHelper extends VideoJSHelper { API_ORIGIN = "https://www.coursera.org/api"; SUBTITLE_SOURCE = "coursera"; /** * Coursera on-demand item types that can embed the same Video.js player * as lectures. */ static VIDEO_ITEM_TYPES = [ "lecture", "ungradedLab", "ungradedWidget", "supplement", "programmingLab", "programmingAssignment", "notebook", "lab", "quiz", "exam", "peer", "discussionPrompt", "honors", "staffGraded", "assignment", "review", "workspaceLab", "ungradedLti", "gradedLti" ]; static LEARN_ITEM_RE = new RegExp(`learn/([^/]+)/(${this.VIDEO_ITEM_TYPES.join("|")})/([^/]+)`); static PREVIEW_LECTURE_RE = /lecture\/([^/]+)\/([^/]+)/; static LEARN_SLUG_RE = /learn\/([^/]+)/; async getCourseData(courseIdOrSlug) { try { const url = typeof courseIdOrSlug === "string" && courseIdOrSlug.includes("-") ? `${this.API_ORIGIN}/onDemandCourses.v1?q=slug&slug=${courseIdOrSlug}` : `${this.API_ORIGIN}/onDemandCourses.v1/${courseIdOrSlug}`; return (await (await this.fetch(url)).json())?.elements?.[0]; } catch (err) { Logger$1.error(`Failed to get course data: ${courseIdOrSlug}`, err.message); return; } } getCourseSlug() { return CourseraHelper.LEARN_SLUG_RE.exec(window.location.pathname)?.[1] ?? CourseraHelper.PREVIEW_LECTURE_RE.exec(window.location.pathname)?.[1]; } getCourseId() { const player = CourseraHelper.getPlayer(); if (player?.options_?.courseId) return player.options_.courseId; const courseraObj = window.coursera; if (typeof courseraObj?.courseId === "string") return courseraObj.courseId; else if (typeof courseraObj?.courseId === "function") try { return courseraObj.courseId(); } catch {} return window.App?.context?.dispatcher?.stores?.CourseStore?.courseId; } static getPlayer() { return VideoJSHelper.getPlayer(); } async getVideoData(videoId) { const data = this.getVideoDataByPlayer(videoId); if (!data) return; const options = CourseraHelper.getPlayer()?.options_; if (!data.subtitles?.length && options?.tracks) data.subtitles = options.tracks.map((track) => ({ url: track.src, language: normalizeLang$1(track.srclang), source: this.SUBTITLE_SOURCE, format: this.SUBTITLE_FORMAT })); const courseIdOrSlug = options?.courseId ?? this.getCourseId() ?? this.getCourseSlug(); let courseLang = "en"; let courseData; if (courseIdOrSlug) courseData = await this.getCourseData(courseIdOrSlug); if (courseData?.primaryLanguageCodes?.[0]) courseLang = normalizeLang$1(courseData.primaryLanguageCodes[0]); else courseLang = normalizeLang$1(document.documentElement.lang || "en"); if (!availableLangs.includes(courseLang)) courseLang = "en"; const subtitleUrl = (data.subtitles.find((subtitle) => subtitle.language === courseLang) ?? data.subtitles?.[0])?.url; if (!subtitleUrl) Logger$1.warn("Failed to find any subtitle file"); const { url, duration } = data; const translationHelp = subtitleUrl ? [{ target: "subtitles_file_url", targetUrl: subtitleUrl }, { target: "video_file_url", targetUrl: url }] : null; return { url: subtitleUrl ? this.service?.url + videoId : url, translationHelp, detectedLanguage: courseLang, duration }; } async getVideoId(url) { return CourseraHelper.LEARN_ITEM_RE.exec(url.pathname)?.[0] ?? CourseraHelper.PREVIEW_LECTURE_RE.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/dailymotion.js var DailymotionHelper = class extends BaseHelper { getVideoIdFromUrl(url) { const videoIdFromQuery = url.searchParams.get("video"); if (videoIdFromQuery) return videoIdFromQuery; } resolveVideoIdViaPostMessage() { return new Promise((resolve) => { const origin = "https://www.dailymotion.com"; const timeout = setTimeout(() => { window.removeEventListener("message", onMessage); resolve(void 0); }, 3e3); const onMessage = (e) => { if (e.origin !== origin) return; if (!(typeof e.data === "string" && e.data.startsWith("getVideoId:"))) return; clearTimeout(timeout); window.removeEventListener("message", onMessage); resolve(e.data.replace("getVideoId:", "")); }; window.addEventListener("message", onMessage); window.top?.postMessage("getVideoId:", origin); }); } async getVideoId(url) { const videoIdFromUrl = this.getVideoIdFromUrl(url); if (videoIdFromUrl) return videoIdFromUrl; if (window.self !== window.top) return await this.resolveVideoIdViaPostMessage(); return url.hostname === "dai.ly" ? url.pathname.slice(1) : /video\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/datacamp.js var DataCampHelper = class extends VideoJSHelper { SUBTITLE_SOURCE = "datacamp"; getVideoUrlFromInput(input) { try { if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement || input.tagName === "INPUT" || input.tagName === "TEXTAREA")) return null; const value = input.value; if (!value) return null; const meta = JSON.parse(value); const videoUrl = meta?.video_url ?? meta?.plain_video_mp4_link ?? meta?.plain_video_hls_link ?? meta?.video_mp4_link ?? meta?.video_hls_link; return typeof videoUrl === "string" ? videoUrl : null; } catch { return null; } } getVideoUrlFromDocument(doc = document) { const videoDataInput = doc.querySelector("#videoData"); if (videoDataInput) { const url = this.getVideoUrlFromInput(videoDataInput); if (url) return url; } const slideDeckInput = doc.querySelector("#slideDeckData"); if (slideDeckInput) { const url = this.getVideoUrlFromInput(slideDeckInput); if (url) return url; } return null; } async getVideoData(videoId) { const cleanUrl = videoId.split("||")[0]; let videoUrl = this.getVideoUrlFromDocument(document); if (!videoUrl) try { const response = await fetch(cleanUrl); if (response.ok) { const html = await response.text(); const doc = new DOMParser().parseFromString(html, "text/html"); videoUrl = this.getVideoUrlFromDocument(doc); } } catch (err) { Logger$1.error("Failed to fetch DataCamp page for DOMParser", err instanceof Error ? err.message : String(err)); } if (!videoUrl) return; return { url: videoId, video_url: videoUrl, translationHelp: [{ target: "video_file_url", targetUrl: videoUrl }] }; } async getVideoId(url) { return url.href; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/deeplearningai.js var DeeplearningAIHelper = class extends BaseHelper { async getVideoData(_videoId) { if (!this.video) return; const sourceUrl = this.video.querySelector("source[type=\"application/x-mpegurl\"]")?.src.replace(/\.m3u8/, "_360p.mp4"); if (!sourceUrl) return; return { url: sourceUrl }; } async getVideoId(url) { return /courses\/(([^/]+)\/lesson\/([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/douyin.js var DouyinHelper = class DouyinHelper extends BaseHelper { static getPlayer() { if (typeof player === "undefined") return; return player; } async getVideoData(_videoId) { const xgPlayer = DouyinHelper.getPlayer(); if (!xgPlayer) return; const { config: { url: sources, duration, lang, isLive: isStream } } = xgPlayer; if (!sources) return; const source = sources.find((s) => s.src.includes("www.douyin.com/aweme/v1/play/")); if (!source) return; return { url: proxyMedia(source.src), duration, isStream, ...availableLangs.includes(lang) ? { detectedLanguage: lang } : {} }; } async getVideoId(url) { const pathId = /video\/([\d]+)/.exec(url.pathname)?.[0]; if (pathId) return pathId; return DouyinHelper.getPlayer()?.config.vid; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/vimeo.js var VimeoHelper = class extends BaseHelper { API_KEY = ""; DEFAULT_SITE_ORIGIN = "https://vimeo.com"; SITE_ORIGIN = this.service?.url?.slice(0, -1) ?? this.DEFAULT_SITE_ORIGIN; isErrorData(data) { return Object.hasOwn(data, "error"); } isPrivatePlayer() { return this.referer && !this.referer.includes("vimeo.com") && this.origin.endsWith("player.vimeo.com"); } toPublicUrl(videoId) { const [id, hash] = videoId.split(":", 2); return hash ? `${this.DEFAULT_SITE_ORIGIN}/${id}/${hash}` : `${this.DEFAULT_SITE_ORIGIN}/${id}`; } returnPublicBaseData(videoId) { const baseData = this.returnBaseData(videoId); if (!baseData) return; return { ...baseData, url: this.toPublicUrl(videoId) }; } normalizePublicVideoUrl(url, videoId) { try { const parsed = new URL(url); if (parsed.hostname === "player.vimeo.com") return this.toPublicUrl(videoId); if (parsed.hostname.endsWith("vimeo.com")) { const colonMatch = /^\/(\d+):([a-z0-9]+)$/i.exec(parsed.pathname); if (colonMatch) return `${this.DEFAULT_SITE_ORIGIN}/${colonMatch[1]}/${colonMatch[2]}`; } } catch {} return url; } async getViewerData() { try { const data = await (await this.fetch("https://vimeo.com/_next/viewer")).json(); const { apiUrl, jwt } = data; this.API_ORIGIN = `https://${apiUrl}`; this.API_KEY = `jwt ${jwt}`; return data; } catch (err) { Logger$1.error(`Failed to get default viewer data.`, err.message); return false; } } async getVideoInfo(videoId) { try { const params = new URLSearchParams({ fields: "name,link,description,duration" }).toString(); const data = await (await this.fetch(`${this.API_ORIGIN}/videos/${videoId}?${params}`, { headers: { Authorization: this.API_KEY } })).json(); if (this.isErrorData(data)) throw new Error(data.developer_message ?? data.error); return data; } catch (err) { Logger$1.error(`Failed to get video info by video ID: ${videoId}`, err.message); return false; } } async getPrivateVideoSource(files) { try { const { default_cdn, cdns } = files.dash; const cdnUrl = cdns[default_cdn].url; const res = await this.fetch(cdnUrl); if (res.status !== 200) throw new VideoHelperError(await res.text()); const data = await res.json(); const baseUrl = new URL(data.base_url, cdnUrl); const videoData = data.audio.find((v) => v.mime_type === "audio/mp4" && v.format === "dash"); if (!videoData) throw new VideoHelperError("Failed to find video data"); const segmentUrl = videoData.segments?.[0]?.url; if (!segmentUrl) throw new VideoHelperError("Failed to find first segment url"); const [videoName, videoParams] = segmentUrl.split("?", 2); const params = new URLSearchParams(videoParams); params.delete("range"); return new URL(`${videoData.base_url}${videoName}?${params.toString()}`, baseUrl).href; } catch (err) { Logger$1.error(`Failed to get private video source`, err.message); return false; } } async getPrivateVideoInfo(videoId) { try { if (typeof playerConfig === "undefined") return; const videoSource = await this.getPrivateVideoSource(playerConfig.request.files); if (!videoSource) throw new VideoHelperError("Failed to get private video source"); const { video: { title, duration }, request: { text_tracks: subs } } = playerConfig; return { url: `${this.SITE_ORIGIN}/${videoId}`, video_url: videoSource, title, duration, subs }; } catch (err) { Logger$1.error(`Failed to get private video info by video ID: ${videoId}`, err.message); return false; } } async getSubsInfo(videoId) { try { const params = new URLSearchParams({ per_page: "100", fields: "language,type,link" }).toString(); const content = await (await this.fetch(`${this.API_ORIGIN}/videos/${videoId}/texttracks?${params}`, { headers: { Authorization: this.API_KEY } })).json(); if (this.isErrorData(content)) throw new Error(content.developer_message ?? content.error); return content.data; } catch (err) { Logger$1.error(`Failed to get subtitles info by video ID: ${videoId}`, err.message); return []; } } async getVideoData(videoId) { if (videoId.includes("?app_id=")) { const [embedId] = videoId.split("?"); return this.returnBaseData(embedId); } if (this.isPrivatePlayer()) { const videoInfo = await this.getPrivateVideoInfo(videoId); if (!videoInfo) return; const { url, subs, video_url, title, duration } = videoInfo; const subtitles = subs.map((sub) => ({ language: normalizeLang$1(sub.lang), source: "vimeo", format: "vtt", url: new URL(sub.url, this.SITE_ORIGIN).href, isAutoGenerated: sub.lang.includes("autogenerated") })); const translationHelp = subtitles.length ? [{ target: "video_file_url", targetUrl: video_url }, { target: "subtitles_file_url", targetUrl: subtitles[0].url }] : null; return { ...translationHelp ? { url, translationHelp } : { url: video_url }, subtitles, title, duration }; } if (!this.extraInfo) return this.returnPublicBaseData(videoId); if (videoId.includes("/")) videoId = videoId.replace("/", ":"); if (!await this.getViewerData()) return this.returnPublicBaseData(videoId); const videoInfo = await this.getVideoInfo(videoId); if (!videoInfo) return this.returnPublicBaseData(videoId); const subtitles = (await this.getSubsInfo(videoId)).map((caption) => ({ language: normalizeLang$1(caption.language), source: "vimeo", format: "vtt", url: caption.link, isAutoGenerated: caption.language.includes("autogen") })); const { link, duration, name: title, description } = videoInfo; return { url: this.normalizePublicVideoUrl(link, videoId), title, description, subtitles, duration }; } async getVideoId(url) { const normalizedPathname = url.pathname.replace(/\/+$/, ""); const embedId = /video\/[^/]+$/.exec(normalizedPathname)?.[0]; const appId = url.searchParams.get("app_id"); if (embedId && appId) return `${embedId}?app_id=${appId}`; if (this.isPrivatePlayer()) return embedId; if (embedId) { const hash = url.searchParams.get("h"); const videoId = embedId.replace("video/", ""); return hash ? `${videoId}/${hash}` : videoId; } const categoriesVideoId = /channels\/[^/]+\/([^/]+)/.exec(normalizedPathname)?.[1] ?? /groups\/[^/]+\/videos\/([^/]+)/.exec(normalizedPathname)?.[1] ?? /(showcase|album)\/[^/]+\/video\/([^/]+)/.exec(normalizedPathname)?.[2]; if (categoriesVideoId) return categoriesVideoId; return /([^/]+\/)?[^/]+$/.exec(normalizedPathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/dropout.js var CONFIG_URL_RE = /^https:\/\/player\.vimeo\.com\/video\/[a-z0-9_-]+\/config/i; var CONFIG_WAIT_TIMEOUT = 1e4; var CONFIG_WAIT_INTERVAL = 500; function isPlayerConfig(data) { if (!data || typeof data !== "object") return false; const config = data; return Boolean((config.request?.files || config.files) && (config.video || config.request)); } function normalizeConfigUrl(value) { if (!value) return; let url = value.replaceAll("\\/", "/").replaceAll("\\u0026", "&").replaceAll("&", "&"); if (url.startsWith("//")) url = `https:${url}`; if (url.startsWith("/video/")) url = `https://player.vimeo.com${url}`; return CONFIG_URL_RE.test(url) ? url : void 0; } /** * Dropout embeds a private Vimeo player on embed.vhx.tv. The page doesn't * expose `playerConfig`, so the player config url is searched in the loaded * resources and in the page markup, then requested directly. */ function findConfigUrl() { const candidates = []; try { candidates.push(...performance.getEntriesByType("resource").map((entry) => entry.name)); } catch {} candidates.push(...Array.from(document.querySelectorAll("iframe[src], script[src]"), (el) => el.src)); const html = document.documentElement?.innerHTML ?? ""; candidates.push(...html.match(/https?:\\?\/\\?\/player\.vimeo\.com\\?\/video\\?\/\d+\\?\/config[^"'<>\s\\]*/gi) ?? [], ...html.match(/\/video\/\d+\/config[^"'<>\s]*/gi) ?? []); return candidates.map(normalizeConfigUrl).find(Boolean); } function getDomTextTracks() { return Array.from(document.querySelectorAll("track[src][kind=\"captions\"], track[src][kind=\"subtitles\"]"), (track) => ({ lang: track.srclang || track.getAttribute("srclang") || "auto", url: track.src, kind: track.kind })).filter((track) => Boolean(track.url)); } function getCdnUrl(group) { return (group?.default_cdn ? group.cdns?.[group.default_cdn]?.url : void 0) ?? Object.values(group?.cdns ?? {})[0]?.url; } function distanceTo360p(item) { return Math.abs((item.height ?? 360) - 360); } function getTrackCodec(track) { return [ track.codecs, track.codec, track.mime_type, track.id, track.quality ].filter(Boolean).join(" ").toLowerCase(); } var DropoutHelper = class extends VimeoHelper { config; isPrivatePlayer() { return true; } async fetchConfig(configUrl) { try { const res = await this.fetch(configUrl, { credentials: "include", headers: { Accept: "application/json" } }); if (!res.ok) throw new VideoHelperError(`Vimeo config request failed: ${res.status}`); const config = await res.json(); return isPlayerConfig(config) ? config : void 0; } catch (err) { Logger$1.error("Failed to fetch Dropout player config", err.message); return; } } async getConfig() { if (this.config) return this.config; if (typeof playerConfig !== "undefined" && isPlayerConfig(playerConfig)) { this.config = playerConfig; return this.config; } const deadline = Date.now() + CONFIG_WAIT_TIMEOUT; let configUrl = findConfigUrl(); while (!configUrl && Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, CONFIG_WAIT_INTERVAL)); configUrl = findConfigUrl(); } if (!configUrl) return; this.config = await this.fetchConfig(configUrl); return this.config; } async getDashAudioSource(dashCdnUrl) { const res = await this.fetch(dashCdnUrl); if (res.status !== 200) throw new VideoHelperError(await res.text()); const data = await res.json(); const baseUrl = new URL(data.base_url ?? "", dashCdnUrl); const tracks = [...data.audio ?? [], ...data.video ?? []].filter((track) => track.format === "dash" && track.segments?.length); const audioTracks = tracks.filter((track) => track.mime_type === "audio/mp4"); const track = audioTracks.find((t) => /mp4a|aac/.test(getTrackCodec(t))) ?? audioTracks.find((t) => !/opus|vorbis/.test(getTrackCodec(t))) ?? audioTracks[0] ?? tracks[0]; if (!track) return; const segmentUrl = track.segments?.[0]?.url; if (!segmentUrl) throw new VideoHelperError("Failed to find first segment url"); const [segmentName, segmentParams = ""] = segmentUrl.split("?", 2); const params = new URLSearchParams(segmentParams); params.delete("range"); const query = params.toString(); return new URL(`${track.base_url ?? ""}${segmentName}${query ? `?${query}` : ""}`, baseUrl).href; } async getPrivateVideoSource(files) { try { const progressiveSource = files?.progressive?.filter((item) => item.url).reduce((best, item) => !best || distanceTo360p(item) < distanceTo360p(best) ? item : best, void 0)?.url; if (progressiveSource) return progressiveSource; const dashCdnUrl = getCdnUrl(files?.dash); const hlsSource = files?.hls?.url ?? getCdnUrl(files?.hls); if (/(?:[?&]qsr=1(?:&|$)|\/prot\/cXNyPTE(?:\/|$))/i.test(String(dashCdnUrl)) && hlsSource) return hlsSource; if (dashCdnUrl) try { const dashSource = await this.getDashAudioSource(dashCdnUrl); if (dashSource) return dashSource; } catch (err) { Logger$1.error("Failed to get Dropout DASH source, trying HLS", err.message); } if (hlsSource) return hlsSource; throw new VideoHelperError("Failed to find playable private video source"); } catch (err) { Logger$1.error("Failed to get Dropout private video source", err.message); return false; } } async getPrivateVideoInfo(videoId) { try { const config = await this.getConfig(); if (!config) throw new VideoHelperError("Failed to get Vimeo player config"); const request = config.request ?? config; const videoSource = await this.getPrivateVideoSource(request.files ?? config.files); if (!videoSource) throw new VideoHelperError("Failed to get private video source"); const configSubs = request.text_tracks ?? request.textTracks ?? []; const subs = configSubs.length ? configSubs : getDomTextTracks(); const video = config.video ?? {}; return { url: `${this.SITE_ORIGIN}/${videoId}`, video_url: videoSource, title: video.title ?? video.name ?? "", duration: video.duration ?? 0, subs }; } catch (err) { Logger$1.error(`Failed to get Dropout video info by video ID: ${videoId}`, err.message); return false; } } async getVideoId(url) { return /videos\/([^/?#]+)/.exec(url.pathname)?.[1] ?? url.pathname; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/dzen.js var DzenHelper = class extends BaseHelper { async getVideoId(url) { return /video\/watch\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/egghead.js var EggheadHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/epicgames.js var EpicGamesHelper = class extends BaseHelper { API_ORIGIN = "https://dev.epicgames.com/community/api/learning"; async getPostInfo(videoId) { try { return await (await this.fetch(`${this.API_ORIGIN}/post.json?hash_id=${videoId}`)).json(); } catch (err) { Logger$1.error(`Failed to get epicgames post info by videoId: ${videoId}.`, err.message); return false; } } getVideoBlock() { const videoUrlRe = /videoUrl\s?=\s"([^"]+)"?/; const script = Array.from(document.body.querySelectorAll("script")).find((s) => videoUrlRe.exec(s.innerHTML)); if (!script) return; const content = script.innerHTML.trim(); const playlistUrl = videoUrlRe.exec(content)?.[1]?.replace("qsep://", "https://"); if (!playlistUrl) return; let subtitlesString = /sources\s?=\s(\[([^\]]+)\])?/.exec(content)?.[1]; if (!subtitlesString) return { playlistUrl, subtitles: [] }; try { subtitlesString = `${subtitlesString.replace(/src:(\s)+?(videoUrl)/g, "src:\"removed\"").substring(0, subtitlesString.lastIndexOf("},"))}]`.split("\n").map((line) => line.replace(/([^\s]+):\s?(?!.*\1)/, "\"$1\":")).join("\n"); return { playlistUrl, subtitles: JSON.parse(subtitlesString).filter((sub) => sub.type === "captions") }; } catch { return { playlistUrl, subtitles: [] }; } } async getVideoData(videoId) { const courseId = videoId.split(":")?.[1]; const postInfo = await this.getPostInfo(courseId); if (!postInfo) return; const videoBlock = this.getVideoBlock(); if (!videoBlock) return; const { playlistUrl, subtitles: videoSubtitles } = videoBlock; const { title, description } = postInfo; return { url: playlistUrl, title, description, subtitles: videoSubtitles.map((caption) => ({ language: normalizeLang$1(caption.srclang), source: "epicgames", format: "vtt", url: caption.src })) }; } async getVideoId(_url) { return new Promise((resolve) => { const origin = "https://dev.epicgames.com"; const reqId = btoa(window.location.href); window.addEventListener("message", (e) => { if (e.origin !== origin) return; if (!(typeof e.data === "string" && e.data.startsWith("getVideoId:"))) return; return resolve(e.data.replace("getVideoId:", "")); }); window.top?.postMessage(`getVideoId:${reqId}`, origin); }); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/eporner.js var EpornerHelper = class extends BaseHelper { async getVideoId(url) { return /video-([^/]+)\/([^/]+)/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/facebook.js var FacebookHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/googledrive.js var GoogleDriveHelper = class extends BaseHelper { getPlayerData() { return document.querySelector("#movie_player")?.getVideoData?.() ?? void 0; } async getVideoId(_url) { return this.getPlayerData()?.video_id; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/ign.js var IgnHelper = class extends BaseHelper { getVideoDataBySource(videoId) { const url = document.querySelector(".icms.video > source[type=\"video/mp4\"][data-quality=\"360\"]")?.src; if (!url) return this.returnBaseData(videoId); return { url: proxyMedia(url) }; } getVideoDataByNext(videoId) { try { const nextContent = document.getElementById("__NEXT_DATA__")?.textContent; if (!nextContent) throw new VideoDataError("Not found __NEXT_DATA__ content"); const { props: { pageProps: { page: { description, title, video: { videoMetadata: { duration }, assets } } } } } = JSON.parse(nextContent); const videoUrl = assets.find((asset) => asset.height === 360 && asset.url.includes(".mp4"))?.url; if (!videoUrl) throw new VideoDataError("Not found video URL in assets"); return { url: proxyMedia(videoUrl), duration, title, description }; } catch (err) { Logger$1.warn(`Failed to get ign video data by video ID: ${videoId}, because ${err.message}. Using clear link instead...`); return this.returnBaseData(videoId); } } async getVideoData(videoId) { if (document.getElementById("__NEXT_DATA__")) return this.getVideoDataByNext(videoId); return this.getVideoDataBySource(videoId); } async getVideoId(url) { return /([^/]+)\/([\d]+)\/video\/([^/]+)/.exec(url.pathname)?.[0] ?? /\/videos\/([^/]+)/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/imdb.js var IMDbHelper = class extends BaseHelper { async getVideoId(url) { return /video\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/joidatabase.js var JOIDatabaseHelper = class extends BaseHelper { async getVideoData(videoId) { const baseData = this.returnBaseData(videoId); if (!baseData) return; try { const player = new PlyrHelper().getPlayer(); if (!player) return baseData; const { duration, config: { title } } = player; return { ...baseData, duration: !Number.isNaN(duration) && duration > 0 ? duration : void 0, title }; } catch { Logger$1.error(`Failed to extract video data. Using base data for videoId: ${videoId}`); return baseData; } } async getVideoId(url) { return /\/(?:watch|embed)\/([0-9a-f]+)\/?$/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/jove.js var JoveHelper = class extends BaseHelper { async getVideoId(url) { const match = /^\/(?:[a-z]{2}\/)?v\/(\d+)\/([^/?#]+)/i.exec(url.pathname); return match ? `v/${match[1]}/${match[2]}` : void 0; } async getVideoData(videoId) { if (!videoId) return void 0; let nextData; if (typeof window !== "undefined" && typeof document !== "undefined") { const nextDataEl = document.getElementById("__NEXT_DATA__"); if (nextDataEl?.textContent) try { nextData = JSON.parse(nextDataEl.textContent); } catch (err) { throw new VideoHelperError(`Failed to parse DOM __NEXT_DATA__: ${err.message}`); } } const queries = nextData?.props?.pageProps?.dehydratedState?.queries || []; let extracted; for (const q of queries) { const data = q.state?.data; if (!data) continue; for (const target of [data.domain, data]) { const videoObj = target?.videos?.en; if (videoObj?.cdnFile) { extracted = { videoUrl: videoObj.cdnFile, duration: videoObj.lengthSeconds ? parseInt(videoObj.lengthSeconds, 10) : void 0, title: target.title, description: target.titleDescription, subtitles: videoObj.subtitles }; break; } } if (extracted) break; } if (!extracted?.videoUrl) throw new VideoHelperError(`Failed to retrieve video URL for ${videoId}`); const { videoUrl, duration, title, description, subtitles: subtitlesObj } = extracted; const subtitles = subtitlesObj ? Object.entries(subtitlesObj).filter(([, url]) => typeof url === "string").map(([lang, url]) => ({ language: normalizeLang$1(lang), source: "jove", format: "vtt", url, isAutoGenerated: false })) : []; return { url: `https://www.jove.com/${videoId}`, video_url: videoUrl, title, description, duration, subtitles, translationHelp: [{ target: "video_file_url", targetUrl: videoUrl }] }; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/kick.js var KickHelper = class extends BaseHelper { API_ORIGIN = "https://kick.com/api"; async getClipInfo(clipId) { try { const { clip_url: url, duration, title } = (await (await this.fetch(`${this.API_ORIGIN}/v2/clips/${clipId}`)).json()).clip; return { url, duration, title }; } catch (err) { Logger$1.error(`Failed to get kick clip info by clipId: ${clipId}.`, err.message); return; } } async getVideoInfo(videoId) { try { const { source: url, livestream } = await (await this.fetch(`${this.API_ORIGIN}/v1/video/${videoId}`)).json(); const { session_title: title, duration } = livestream; return { url, duration: Math.round(duration / 1e3), title }; } catch (err) { Logger$1.error(`Failed to get kick video info by videoId: ${videoId}.`, err.message); return; } } async getVideoData(videoId) { return videoId.startsWith("videos") ? await this.getVideoInfo(videoId.replace("videos/", "")) : await this.getClipInfo(videoId.replace("clips/", "")); } async getVideoId(url) { return /([^/]+)\/((videos|clips)\/([^/]+))/.exec(url.pathname)?.[2]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/kickstarter.js var KickstarterHelper = class extends BaseHelper { async getVideoData(videoId) { try { const videoEl = document.querySelector(".ksr-video-player > video"); const url = videoEl?.querySelector("source[type^='video/mp4']")?.src; if (!url) throw new VideoHelperError("Failed to find video URL"); const subtitles = videoEl?.querySelectorAll("track") ?? []; return { url, subtitles: Array.from(subtitles).reduce((result, sub) => { const lang = sub.getAttribute("srclang"); const url = sub.getAttribute("src"); if (!lang || !url) return result; result.push({ language: normalizeLang$1(lang), url, format: "vtt", source: "kickstarter" }); return result; }, []) }; } catch (err) { Logger$1.error(`Failed to get Kickstarter data by videoId: ${videoId}`, err.message); return; } } async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/kodik.js var KodikHelper = class extends BaseHelper { API_ORIGIN = window.location.origin; getSecureData(videoPath) { try { const [videoType, videoId, hash] = videoPath.split("/").filter((a) => a); const allScripts = Array.from(document.getElementsByTagName("script")); const secureScript = allScripts.filter((s) => s.innerHTML.includes(`videoId = "${videoId}"`) || s.innerHTML.includes(`serialId = Number(${videoId})`)); if (!secureScript.length) throw new VideoHelperError("Failed to find secure script"); const secureScriptContent = secureScript[0]?.textContent?.trim(); if (!secureScriptContent) throw new VideoHelperError("Secure script content is empty"); const secureContent = /'{[^']+}'/.exec(secureScriptContent)?.[0]; if (!secureContent) throw new VideoHelperError("Secure json wasn't found in secure script"); const secureJSON = JSON.parse(secureContent.replaceAll("'", "")); if (videoType !== "serial") return { videoType, videoId, hash, ...secureJSON }; const videoInfoContent = allScripts.find((s) => s.innerHTML.includes(`var videoInfo = {}`))?.textContent?.trim(); if (!videoInfoContent) throw new VideoHelperError("Failed to find videoInfo content"); const realVideoType = /videoInfo\.type\s+?=\s+?'([^']+)'/.exec(videoInfoContent)?.[1]; const realVideoId = /videoInfo\.id\s+?=\s+?'([^']+)'/.exec(videoInfoContent)?.[1]; const realHash = /videoInfo\.hash\s+?=\s+?'([^']+)'/.exec(videoInfoContent)?.[1]; if (!realVideoType || !realVideoId || !realHash) throw new VideoHelperError("Failed to parse videoInfo content"); return { videoType: realVideoType, videoId: realVideoId, hash: realHash, ...secureJSON }; } catch (err) { Logger$1.error(`Failed to get kodik secure data by videoPath: ${videoPath}.`, err.message); return false; } } async getFtor(secureData) { const { videoType, videoId: id, hash, d, d_sign, pd, pd_sign, ref, ref_sign } = secureData; try { return await (await this.fetch(`${this.API_ORIGIN}/ftor`, { method: "POST", headers: { "User-Agent": config_default$1.userAgent, Origin: this.API_ORIGIN, Referer: `${this.API_ORIGIN}/${videoType}/${id}/${hash}/360p` }, body: new URLSearchParams({ d, d_sign, pd, pd_sign, ref: decodeURIComponent(ref), ref_sign, bad_user: "false", cdn_is_working: "true", info: "{}", type: videoType, hash, id }) })).json(); } catch (err) { Logger$1.error(`Failed to get kodik video data (type: ${videoType}, id: ${id}, hash: ${hash})`, err.message); return false; } } decryptUrl(encryptedUrl) { return `${atob(encryptedUrl.replace(/[a-zA-Z]/g, (e) => { const charCode = e.charCodeAt(0) + 18; const pos = e <= "Z" ? 90 : 122; return String.fromCharCode(pos >= charCode ? charCode : charCode - 26); }))}`; } async getVideoData(videoId) { const secureData = this.getSecureData(videoId); if (!secureData) return; const videoData = await this.getFtor(secureData); if (!videoData) return; const videoLink = Object.entries(videoData.links[videoData.default.toString()]).find(([, data]) => data.type === "application/x-mpegURL")?.[1]; if (!videoLink) return; let videoUrl = videoLink.src.startsWith("//") ? videoLink.src : this.decryptUrl(videoLink.src); if (videoUrl.startsWith("//")) videoUrl = `https:${videoUrl}`; return { url: videoId, video_url: videoUrl, translationHelp: [{ target: "video_file_url", targetUrl: proxyMedia(new URL(videoUrl)) }] }; } async getVideoId(url) { return /\/(uv|video|seria|episode|season|serial)\/([^/]+)\/([^/]+)\/([\d]+)p/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/linkedin.js var LinkedinHelper = class extends VideoJSHelper { SUBTITLE_SOURCE = "linkedin"; async getVideoData(videoId) { const data = this.getVideoDataByPlayer(videoId); if (!data) return; const { url, duration, subtitles } = data; return { url: proxyMedia(new URL(url)), duration, subtitles }; } async getVideoId(url) { return /\/learning\/(([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@bufbuild/protobuf/dist/esm/wire/varint.js /** * Read a 64 bit varint as two JS numbers. * * Returns tuple: * [0]: low bits * [1]: high bits * * Copyright 2008 Google Inc. All rights reserved. * * See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/buffer_decoder.js#L175 */ function varint64read() { let lowBits = 0; let highBits = 0; for (let shift = 0; shift < 28; shift += 7) { let b = this.buf[this.pos++]; lowBits |= (b & 127) << shift; if ((b & 128) == 0) { this.assertBounds(); return [lowBits, highBits]; } } let middleByte = this.buf[this.pos++]; lowBits |= (middleByte & 15) << 28; highBits = (middleByte & 112) >> 4; if ((middleByte & 128) == 0) { this.assertBounds(); return [lowBits, highBits]; } for (let shift = 3; shift <= 31; shift += 7) { let b = this.buf[this.pos++]; highBits |= (b & 127) << shift; if ((b & 128) == 0) { this.assertBounds(); return [lowBits, highBits]; } } throw new Error("invalid varint"); } /** * Write a 64 bit varint, given as two JS numbers, to the given bytes array. * * Copyright 2008 Google Inc. All rights reserved. * * See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/writer.js#L344 */ function varint64write(lo, hi, bytes) { for (let i = 0; i < 28; i = i + 7) { const shift = lo >>> i; const hasNext = !(shift >>> 7 == 0 && hi == 0); const byte = (hasNext ? shift | 128 : shift) & 255; bytes.push(byte); if (!hasNext) return; } const splitBits = lo >>> 28 & 15 | (hi & 7) << 4; const hasMoreBits = !(hi >> 3 == 0); bytes.push((hasMoreBits ? splitBits | 128 : splitBits) & 255); if (!hasMoreBits) return; for (let i = 3; i < 31; i = i + 7) { const shift = hi >>> i; const hasNext = !(shift >>> 7 == 0); const byte = (hasNext ? shift | 128 : shift) & 255; bytes.push(byte); if (!hasNext) return; } bytes.push(hi >>> 31 & 1); } var TWO_PWR_32_DBL = 4294967296; /** * Parse decimal string of 64 bit integer value as two JS numbers. * * Copyright 2008 Google Inc. All rights reserved. * * See https://github.com/protocolbuffers/protobuf-javascript/blob/a428c58273abad07c66071d9753bc4d1289de426/experimental/runtime/int64.js#L10 */ function int64FromString(dec) { const minus = dec[0] === "-"; if (minus) dec = dec.slice(1); const base = 1e6; let lowBits = 0; let highBits = 0; function add1e6digit(begin, end) { const digit1e6 = Number(dec.slice(begin, end)); highBits *= base; lowBits = lowBits * base + digit1e6; if (lowBits >= TWO_PWR_32_DBL) { highBits = highBits + (lowBits / TWO_PWR_32_DBL | 0); lowBits = lowBits % TWO_PWR_32_DBL; } } add1e6digit(-24, -18); add1e6digit(-18, -12); add1e6digit(-12, -6); add1e6digit(-6); return minus ? negate(lowBits, highBits) : newBits(lowBits, highBits); } /** * Losslessly converts a 64-bit signed integer in 32:32 split representation * into a decimal string. * * Copyright 2008 Google Inc. All rights reserved. * * See https://github.com/protocolbuffers/protobuf-javascript/blob/a428c58273abad07c66071d9753bc4d1289de426/experimental/runtime/int64.js#L10 */ function int64ToString(lo, hi) { let bits = newBits(lo, hi); const negative = bits.hi & 2147483648; if (negative) bits = negate(bits.lo, bits.hi); const result = uInt64ToString(bits.lo, bits.hi); return negative ? "-" + result : result; } /** * Losslessly converts a 64-bit unsigned integer in 32:32 split representation * into a decimal string. * * Copyright 2008 Google Inc. All rights reserved. * * See https://github.com/protocolbuffers/protobuf-javascript/blob/a428c58273abad07c66071d9753bc4d1289de426/experimental/runtime/int64.js#L10 */ function uInt64ToString(lo, hi) { ({lo, hi} = toUnsigned(lo, hi)); if (hi <= 2097151) return String(TWO_PWR_32_DBL * hi + lo); const low = lo & 16777215; const mid = (lo >>> 24 | hi << 8) & 16777215; const high = hi >> 16 & 65535; let digitA = low + mid * 6777216 + high * 6710656; let digitB = mid + high * 8147497; let digitC = high * 2; const base = 1e7; if (digitA >= base) { digitB += Math.floor(digitA / base); digitA %= base; } if (digitB >= base) { digitC += Math.floor(digitB / base); digitB %= base; } return digitC.toString() + decimalFrom1e7WithLeadingZeros(digitB) + decimalFrom1e7WithLeadingZeros(digitA); } function toUnsigned(lo, hi) { return { lo: lo >>> 0, hi: hi >>> 0 }; } function newBits(lo, hi) { return { lo: lo | 0, hi: hi | 0 }; } /** * Returns two's compliment negation of input. * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Signed_32-bit_integers */ function negate(lowBits, highBits) { highBits = ~highBits; if (lowBits) lowBits = ~lowBits + 1; else highBits += 1; return newBits(lowBits, highBits); } /** * Returns decimal representation of digit1e7 with leading zeros. */ var decimalFrom1e7WithLeadingZeros = (digit1e7) => { const partial = String(digit1e7); return "0000000".slice(partial.length) + partial; }; /** * Write a 32 bit varint, signed or unsigned. Same as `varint64write(0, value, bytes)` * * Copyright 2008 Google Inc. All rights reserved. * * See https://github.com/protocolbuffers/protobuf/blob/1b18833f4f2a2f681f4e4a25cdf3b0a43115ec26/js/binary/encoder.js#L144 */ function varint32write(value, bytes) { if (value >= 0) { while (value > 127) { bytes.push(value & 127 | 128); value = value >>> 7; } bytes.push(value); } else { for (let i = 0; i < 9; i++) { bytes.push(value & 127 | 128); value = value >> 7; } bytes.push(1); } } /** * Read an unsigned 32 bit varint. * * See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/buffer_decoder.js#L220 */ function varint32read() { let b = this.buf[this.pos++]; let result = b & 127; if ((b & 128) == 0) { this.assertBounds(); return result; } b = this.buf[this.pos++]; result |= (b & 127) << 7; if ((b & 128) == 0) { this.assertBounds(); return result; } b = this.buf[this.pos++]; result |= (b & 127) << 14; if ((b & 128) == 0) { this.assertBounds(); return result; } b = this.buf[this.pos++]; result |= (b & 127) << 21; if ((b & 128) == 0) { this.assertBounds(); return result; } b = this.buf[this.pos++]; result |= (b & 15) << 28; for (let readBytes = 5; (b & 128) !== 0 && readBytes < 10; readBytes++) b = this.buf[this.pos++]; if ((b & 128) != 0) throw new Error("invalid varint"); this.assertBounds(); return result >>> 0; } //#endregion //#region node_modules/@bufbuild/protobuf/dist/esm/proto-int64.js /** * Int64Support for the current environment. */ var protoInt64 = /*@__PURE__*/ makeInt64Support(); function makeInt64Support() { const dv = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8)); if (typeof BigInt === "function" && typeof dv.getBigInt64 === "function" && typeof dv.getBigUint64 === "function" && typeof dv.setBigInt64 === "function" && typeof dv.setBigUint64 === "function" && (typeof process != "object" || typeof process.env != "object" || process.env.BUF_BIGINT_DISABLE !== "1")) { const MIN = BigInt("-9223372036854775808"), MAX = BigInt("9223372036854775807"), UMIN = BigInt("0"), UMAX = BigInt("18446744073709551615"); return { zero: BigInt(0), supported: true, parse(value) { const bi = typeof value == "bigint" ? value : BigInt(value); if (bi > MAX || bi < MIN) throw new Error(`invalid int64: ${value}`); return bi; }, uParse(value) { const bi = typeof value == "bigint" ? value : BigInt(value); if (bi > UMAX || bi < UMIN) throw new Error(`invalid uint64: ${value}`); return bi; }, enc(value) { dv.setBigInt64(0, this.parse(value), true); return { lo: dv.getInt32(0, true), hi: dv.getInt32(4, true) }; }, uEnc(value) { dv.setBigInt64(0, this.uParse(value), true); return { lo: dv.getInt32(0, true), hi: dv.getInt32(4, true) }; }, dec(lo, hi) { dv.setInt32(0, lo, true); dv.setInt32(4, hi, true); return dv.getBigInt64(0, true); }, uDec(lo, hi) { dv.setInt32(0, lo, true); dv.setInt32(4, hi, true); return dv.getBigUint64(0, true); } }; } return { zero: "0", supported: false, parse(value) { if (typeof value != "string") value = value.toString(); assertInt64String(value); return value; }, uParse(value) { if (typeof value != "string") value = value.toString(); assertUInt64String(value); return value; }, enc(value) { if (typeof value != "string") value = value.toString(); assertInt64String(value); return int64FromString(value); }, uEnc(value) { if (typeof value != "string") value = value.toString(); assertUInt64String(value); return int64FromString(value); }, dec(lo, hi) { return int64ToString(lo, hi); }, uDec(lo, hi) { return uInt64ToString(lo, hi); } }; } function assertInt64String(value) { if (!/^-?[0-9]+$/.test(value)) throw new Error("invalid int64: " + value); } function assertUInt64String(value) { if (!/^[0-9]+$/.test(value)) throw new Error("invalid uint64: " + value); } //#endregion //#region node_modules/@bufbuild/protobuf/dist/esm/wire/text-encoding.js var symbol = Symbol.for("@bufbuild/protobuf/text-encoding"); function getTextEncoding() { if (globalThis[symbol] == void 0) { const te = new globalThis.TextEncoder(); const td = new globalThis.TextDecoder(); globalThis[symbol] = { encodeUtf8(text) { return te.encode(text); }, decodeUtf8(bytes) { return td.decode(bytes); }, checkUtf8(text) { try { return true; } catch (e) { return false; } } }; } return globalThis[symbol]; } //#endregion //#region node_modules/@bufbuild/protobuf/dist/esm/wire/binary-encoding.js /** * Protobuf binary format wire types. * * A wire type provides just enough information to find the length of the * following value. * * See https://developers.google.com/protocol-buffers/docs/encoding#structure */ var WireType; (function(WireType) { /** * Used for int32, int64, uint32, uint64, sint32, sint64, bool, enum */ WireType[WireType["Varint"] = 0] = "Varint"; /** * Used for fixed64, sfixed64, double. * Always 8 bytes with little-endian byte order. */ WireType[WireType["Bit64"] = 1] = "Bit64"; /** * Used for string, bytes, embedded messages, packed repeated fields * * Only repeated numeric types (types which use the varint, 32-bit, * or 64-bit wire types) can be packed. In proto3, such fields are * packed by default. */ WireType[WireType["LengthDelimited"] = 2] = "LengthDelimited"; /** * Start of a tag-delimited aggregate, such as a proto2 group, or a message * in editions with message_encoding = DELIMITED. */ WireType[WireType["StartGroup"] = 3] = "StartGroup"; /** * End of a tag-delimited aggregate. */ WireType[WireType["EndGroup"] = 4] = "EndGroup"; /** * Used for fixed32, sfixed32, float. * Always 4 bytes with little-endian byte order. */ WireType[WireType["Bit32"] = 5] = "Bit32"; })(WireType || (WireType = {})); var BinaryWriter = class { constructor(encodeUtf8 = getTextEncoding().encodeUtf8) { this.encodeUtf8 = encodeUtf8; /** * Previous fork states. */ this.stack = []; this.chunks = []; this.buf = []; } /** * Return all bytes written and reset this writer. */ finish() { if (this.buf.length) { this.chunks.push(new Uint8Array(this.buf)); this.buf = []; } let len = 0; for (let i = 0; i < this.chunks.length; i++) len += this.chunks[i].length; let bytes = new Uint8Array(len); let offset = 0; for (let i = 0; i < this.chunks.length; i++) { bytes.set(this.chunks[i], offset); offset += this.chunks[i].length; } this.chunks = []; return bytes; } /** * Start a new fork for length-delimited data like a message * or a packed repeated field. * * Must be joined later with `join()`. */ fork() { this.stack.push({ chunks: this.chunks, buf: this.buf }); this.chunks = []; this.buf = []; return this; } /** * Join the last fork. Write its length and bytes, then * return to the previous state. */ join() { let chunk = this.finish(); let prev = this.stack.pop(); if (!prev) throw new Error("invalid state, fork stack empty"); this.chunks = prev.chunks; this.buf = prev.buf; this.uint32(chunk.byteLength); return this.raw(chunk); } /** * Writes a tag (field number and wire type). * * Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`. * * Generated code should compute the tag ahead of time and call `uint32()`. */ tag(fieldNo, type) { return this.uint32((fieldNo << 3 | type) >>> 0); } /** * Write a chunk of raw bytes. */ raw(chunk) { if (this.buf.length) { this.chunks.push(new Uint8Array(this.buf)); this.buf = []; } this.chunks.push(chunk); return this; } /** * Write a `uint32` value, an unsigned 32 bit varint. */ uint32(value) { assertUInt32(value); while (value > 127) { this.buf.push(value & 127 | 128); value = value >>> 7; } this.buf.push(value); return this; } /** * Write a `int32` value, a signed 32 bit varint. */ int32(value) { assertInt32(value); varint32write(value, this.buf); return this; } /** * Write a `bool` value, a variant. */ bool(value) { this.buf.push(value ? 1 : 0); return this; } /** * Write a `bytes` value, length-delimited arbitrary data. */ bytes(value) { this.uint32(value.byteLength); return this.raw(value); } /** * Write a `string` value, length-delimited data converted to UTF-8 text. */ string(value) { let chunk = this.encodeUtf8(value); this.uint32(chunk.byteLength); return this.raw(chunk); } /** * Write a `float` value, 32-bit floating point number. */ float(value) { assertFloat32(value); let chunk = /* @__PURE__ */ new Uint8Array(4); new DataView(chunk.buffer).setFloat32(0, value, true); return this.raw(chunk); } /** * Write a `double` value, a 64-bit floating point number. */ double(value) { let chunk = /* @__PURE__ */ new Uint8Array(8); new DataView(chunk.buffer).setFloat64(0, value, true); return this.raw(chunk); } /** * Write a `fixed32` value, an unsigned, fixed-length 32-bit integer. */ fixed32(value) { assertUInt32(value); let chunk = /* @__PURE__ */ new Uint8Array(4); new DataView(chunk.buffer).setUint32(0, value, true); return this.raw(chunk); } /** * Write a `sfixed32` value, a signed, fixed-length 32-bit integer. */ sfixed32(value) { assertInt32(value); let chunk = /* @__PURE__ */ new Uint8Array(4); new DataView(chunk.buffer).setInt32(0, value, true); return this.raw(chunk); } /** * Write a `sint32` value, a signed, zigzag-encoded 32-bit varint. */ sint32(value) { assertInt32(value); value = (value << 1 ^ value >> 31) >>> 0; varint32write(value, this.buf); return this; } /** * Write a `fixed64` value, a signed, fixed-length 64-bit integer. */ sfixed64(value) { let chunk = /* @__PURE__ */ new Uint8Array(8), view = new DataView(chunk.buffer), tc = protoInt64.enc(value); view.setInt32(0, tc.lo, true); view.setInt32(4, tc.hi, true); return this.raw(chunk); } /** * Write a `fixed64` value, an unsigned, fixed-length 64 bit integer. */ fixed64(value) { let chunk = /* @__PURE__ */ new Uint8Array(8), view = new DataView(chunk.buffer), tc = protoInt64.uEnc(value); view.setInt32(0, tc.lo, true); view.setInt32(4, tc.hi, true); return this.raw(chunk); } /** * Write a `int64` value, a signed 64-bit varint. */ int64(value) { let tc = protoInt64.enc(value); varint64write(tc.lo, tc.hi, this.buf); return this; } /** * Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint. */ sint64(value) { let tc = protoInt64.enc(value), sign = tc.hi >> 31; varint64write(tc.lo << 1 ^ sign, (tc.hi << 1 | tc.lo >>> 31) ^ sign, this.buf); return this; } /** * Write a `uint64` value, an unsigned 64-bit varint. */ uint64(value) { let tc = protoInt64.uEnc(value); varint64write(tc.lo, tc.hi, this.buf); return this; } }; var BinaryReader = class { constructor(buf, decodeUtf8 = getTextEncoding().decodeUtf8) { this.decodeUtf8 = decodeUtf8; this.varint64 = varint64read; /** * Read a `uint32` field, an unsigned 32 bit varint. */ this.uint32 = varint32read; this.buf = buf; this.len = buf.length; this.pos = 0; this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); } /** * Reads a tag - field number and wire type. */ tag() { let tag = this.uint32(), fieldNo = tag >>> 3, wireType = tag & 7; if (fieldNo <= 0 || wireType < 0 || wireType > 5) throw new Error("illegal tag: field no " + fieldNo + " wire type " + wireType); return [fieldNo, wireType]; } /** * Skip one element and return the skipped data. * * When skipping StartGroup, provide the tags field number to check for * matching field number in the EndGroup tag. */ skip(wireType, fieldNo) { let start = this.pos; switch (wireType) { case WireType.Varint: while (this.buf[this.pos++] & 128); break; case WireType.Bit64: this.pos += 4; case WireType.Bit32: this.pos += 4; break; case WireType.LengthDelimited: let len = this.uint32(); this.pos += len; break; case WireType.StartGroup: for (;;) { const [fn, wt] = this.tag(); if (wt === WireType.EndGroup) { if (fieldNo !== void 0 && fn !== fieldNo) throw new Error("invalid end group tag"); break; } this.skip(wt, fn); } break; default: throw new Error("cant skip wire type " + wireType); } this.assertBounds(); return this.buf.subarray(start, this.pos); } /** * Throws error if position in byte array is out of range. */ assertBounds() { if (this.pos > this.len) throw new RangeError("premature EOF"); } /** * Read a `int32` field, a signed 32 bit varint. */ int32() { return this.uint32() | 0; } /** * Read a `sint32` field, a signed, zigzag-encoded 32-bit varint. */ sint32() { let zze = this.uint32(); return zze >>> 1 ^ -(zze & 1); } /** * Read a `int64` field, a signed 64-bit varint. */ int64() { return protoInt64.dec(...this.varint64()); } /** * Read a `uint64` field, an unsigned 64-bit varint. */ uint64() { return protoInt64.uDec(...this.varint64()); } /** * Read a `sint64` field, a signed, zig-zag-encoded 64-bit varint. */ sint64() { let [lo, hi] = this.varint64(); let s = -(lo & 1); lo = (lo >>> 1 | (hi & 1) << 31) ^ s; hi = hi >>> 1 ^ s; return protoInt64.dec(lo, hi); } /** * Read a `bool` field, a variant. */ bool() { let [lo, hi] = this.varint64(); return lo !== 0 || hi !== 0; } /** * Read a `fixed32` field, an unsigned, fixed-length 32-bit integer. */ fixed32() { return this.view.getUint32((this.pos += 4) - 4, true); } /** * Read a `sfixed32` field, a signed, fixed-length 32-bit integer. */ sfixed32() { return this.view.getInt32((this.pos += 4) - 4, true); } /** * Read a `fixed64` field, an unsigned, fixed-length 64 bit integer. */ fixed64() { return protoInt64.uDec(this.sfixed32(), this.sfixed32()); } /** * Read a `fixed64` field, a signed, fixed-length 64-bit integer. */ sfixed64() { return protoInt64.dec(this.sfixed32(), this.sfixed32()); } /** * Read a `float` field, 32-bit floating point number. */ float() { return this.view.getFloat32((this.pos += 4) - 4, true); } /** * Read a `double` field, a 64-bit floating point number. */ double() { return this.view.getFloat64((this.pos += 8) - 8, true); } /** * Read a `bytes` field, length-delimited arbitrary data. */ bytes() { let len = this.uint32(), start = this.pos; this.pos += len; this.assertBounds(); return this.buf.subarray(start, start + len); } /** * Read a `string` field, length-delimited data converted to UTF-8 text. */ string() { return this.decodeUtf8(this.bytes()); } }; /** * Assert a valid signed protobuf 32-bit integer as a number or string. */ function assertInt32(arg) { if (typeof arg == "string") arg = Number(arg); else if (typeof arg != "number") throw new Error("invalid int32: " + typeof arg); if (!Number.isInteger(arg) || arg > 2147483647 || arg < -2147483648) throw new Error("invalid int32: " + arg); } /** * Assert a valid unsigned protobuf 32-bit integer as a number or string. */ function assertUInt32(arg) { if (typeof arg == "string") arg = Number(arg); else if (typeof arg != "number") throw new Error("invalid uint32: " + typeof arg); if (!Number.isInteger(arg) || arg > 4294967295 || arg < 0) throw new Error("invalid uint32: " + arg); } /** * Assert a valid protobuf float value as a number or string. */ function assertFloat32(arg) { if (typeof arg == "string") { const o = arg; arg = Number(arg); if (isNaN(arg) && o !== "NaN") throw new Error("invalid float32: " + o); } else if (typeof arg != "number") throw new Error("invalid float32: " + typeof arg); if (Number.isFinite(arg) && (arg > 34028234663852886e22 || arg < -34028234663852886e22)) throw new Error("invalid float32: " + arg); } //#endregion //#region node_modules/@vot.js/shared/dist/protos/yandex.js var StreamInterval; (function(StreamInterval) { StreamInterval[StreamInterval["NO_CONNECTION"] = 0] = "NO_CONNECTION"; StreamInterval[StreamInterval["TRANSLATING"] = 10] = "TRANSLATING"; StreamInterval[StreamInterval["STREAMING"] = 20] = "STREAMING"; StreamInterval[StreamInterval["UNRECOGNIZED"] = -1] = "UNRECOGNIZED"; })(StreamInterval || (StreamInterval = {})); function streamIntervalFromJSON(object) { switch (object) { case 0: case "NO_CONNECTION": return StreamInterval.NO_CONNECTION; case 10: case "TRANSLATING": return StreamInterval.TRANSLATING; case 20: case "STREAMING": return StreamInterval.STREAMING; default: return StreamInterval.UNRECOGNIZED; } } function streamIntervalToJSON(object) { switch (object) { case StreamInterval.NO_CONNECTION: return "NO_CONNECTION"; case StreamInterval.TRANSLATING: return "TRANSLATING"; case StreamInterval.STREAMING: return "STREAMING"; case StreamInterval.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } function createBaseVideoTranslationHelpObject() { return { target: "", targetUrl: "" }; } var VideoTranslationHelpObject = { encode(message, writer = new BinaryWriter()) { if (message.target !== "") writer.uint32(10).string(message.target); if (message.targetUrl !== "") writer.uint32(18).string(message.targetUrl); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationHelpObject(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.target = reader.string(); continue; case 2: if (tag !== 18) break; message.targetUrl = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { target: isSet(object.target) ? globalThis.String(object.target) : "", targetUrl: isSet(object.targetUrl) ? globalThis.String(object.targetUrl) : "" }; }, toJSON(message) { const obj = {}; if (message.target !== "") obj.target = message.target; if (message.targetUrl !== "") obj.targetUrl = message.targetUrl; return obj; }, create(base) { return VideoTranslationHelpObject.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationHelpObject(); message.target = object.target ?? ""; message.targetUrl = object.targetUrl ?? ""; return message; } }; function createBaseVideoTranslationRequest() { return { url: "", deviceId: void 0, firstRequest: false, duration: 0, unknown0: false, language: "", forceSourceLang: false, unknown1: false, translationHelp: [], wasStream: false, responseLanguage: "", unknown2: false, configVersion: 0, bypassCache: false, useLivelyVoice: false, videoTitle: "" }; } var VideoTranslationRequest = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(26).string(message.url); if (message.deviceId !== void 0) writer.uint32(34).string(message.deviceId); if (message.firstRequest !== false) writer.uint32(40).bool(message.firstRequest); if (message.duration !== 0) writer.uint32(49).double(message.duration); if (message.unknown0 !== false) writer.uint32(56).bool(message.unknown0); if (message.language !== "") writer.uint32(66).string(message.language); if (message.forceSourceLang !== false) writer.uint32(72).bool(message.forceSourceLang); if (message.unknown1 !== false) writer.uint32(80).bool(message.unknown1); for (const v of message.translationHelp) VideoTranslationHelpObject.encode(v, writer.uint32(90).fork()).join(); if (message.wasStream !== false) writer.uint32(104).bool(message.wasStream); if (message.responseLanguage !== "") writer.uint32(114).string(message.responseLanguage); if (message.unknown2 !== false) writer.uint32(120).bool(message.unknown2); if (message.configVersion !== 0) writer.uint32(128).int32(message.configVersion); if (message.bypassCache !== false) writer.uint32(136).bool(message.bypassCache); if (message.useLivelyVoice !== false) writer.uint32(144).bool(message.useLivelyVoice); if (message.videoTitle !== "") writer.uint32(154).string(message.videoTitle); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 3: if (tag !== 26) break; message.url = reader.string(); continue; case 4: if (tag !== 34) break; message.deviceId = reader.string(); continue; case 5: if (tag !== 40) break; message.firstRequest = reader.bool(); continue; case 6: if (tag !== 49) break; message.duration = reader.double(); continue; case 7: if (tag !== 56) break; message.unknown0 = reader.bool(); continue; case 8: if (tag !== 66) break; message.language = reader.string(); continue; case 9: if (tag !== 72) break; message.forceSourceLang = reader.bool(); continue; case 10: if (tag !== 80) break; message.unknown1 = reader.bool(); continue; case 11: if (tag !== 90) break; message.translationHelp.push(VideoTranslationHelpObject.decode(reader, reader.uint32())); continue; case 13: if (tag !== 104) break; message.wasStream = reader.bool(); continue; case 14: if (tag !== 114) break; message.responseLanguage = reader.string(); continue; case 15: if (tag !== 120) break; message.unknown2 = reader.bool(); continue; case 16: if (tag !== 128) break; message.configVersion = reader.int32(); continue; case 17: if (tag !== 136) break; message.bypassCache = reader.bool(); continue; case 18: if (tag !== 144) break; message.useLivelyVoice = reader.bool(); continue; case 19: if (tag !== 154) break; message.videoTitle = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", deviceId: isSet(object.deviceId) ? globalThis.String(object.deviceId) : void 0, firstRequest: isSet(object.firstRequest) ? globalThis.Boolean(object.firstRequest) : false, duration: isSet(object.duration) ? globalThis.Number(object.duration) : 0, unknown0: isSet(object.unknown0) ? globalThis.Boolean(object.unknown0) : false, language: isSet(object.language) ? globalThis.String(object.language) : "", forceSourceLang: isSet(object.forceSourceLang) ? globalThis.Boolean(object.forceSourceLang) : false, unknown1: isSet(object.unknown1) ? globalThis.Boolean(object.unknown1) : false, translationHelp: globalThis.Array.isArray(object?.translationHelp) ? object.translationHelp.map((e) => VideoTranslationHelpObject.fromJSON(e)) : [], wasStream: isSet(object.wasStream) ? globalThis.Boolean(object.wasStream) : false, responseLanguage: isSet(object.responseLanguage) ? globalThis.String(object.responseLanguage) : "", unknown2: isSet(object.unknown2) ? globalThis.Boolean(object.unknown2) : false, configVersion: isSet(object.configVersion) ? globalThis.Number(object.configVersion) : 0, bypassCache: isSet(object.bypassCache) ? globalThis.Boolean(object.bypassCache) : false, useLivelyVoice: isSet(object.useLivelyVoice) ? globalThis.Boolean(object.useLivelyVoice) : false, videoTitle: isSet(object.videoTitle) ? globalThis.String(object.videoTitle) : "" }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.deviceId !== void 0) obj.deviceId = message.deviceId; if (message.firstRequest !== false) obj.firstRequest = message.firstRequest; if (message.duration !== 0) obj.duration = message.duration; if (message.unknown0 !== false) obj.unknown0 = message.unknown0; if (message.language !== "") obj.language = message.language; if (message.forceSourceLang !== false) obj.forceSourceLang = message.forceSourceLang; if (message.unknown1 !== false) obj.unknown1 = message.unknown1; if (message.translationHelp?.length) obj.translationHelp = message.translationHelp.map((e) => VideoTranslationHelpObject.toJSON(e)); if (message.wasStream !== false) obj.wasStream = message.wasStream; if (message.responseLanguage !== "") obj.responseLanguage = message.responseLanguage; if (message.unknown2 !== false) obj.unknown2 = message.unknown2; if (message.configVersion !== 0) obj.configVersion = Math.round(message.configVersion); if (message.bypassCache !== false) obj.bypassCache = message.bypassCache; if (message.useLivelyVoice !== false) obj.useLivelyVoice = message.useLivelyVoice; if (message.videoTitle !== "") obj.videoTitle = message.videoTitle; return obj; }, create(base) { return VideoTranslationRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationRequest(); message.url = object.url ?? ""; message.deviceId = object.deviceId ?? void 0; message.firstRequest = object.firstRequest ?? false; message.duration = object.duration ?? 0; message.unknown0 = object.unknown0 ?? false; message.language = object.language ?? ""; message.forceSourceLang = object.forceSourceLang ?? false; message.unknown1 = object.unknown1 ?? false; message.translationHelp = object.translationHelp?.map((e) => VideoTranslationHelpObject.fromPartial(e)) || []; message.wasStream = object.wasStream ?? false; message.responseLanguage = object.responseLanguage ?? ""; message.unknown2 = object.unknown2 ?? false; message.configVersion = object.configVersion ?? 0; message.bypassCache = object.bypassCache ?? false; message.useLivelyVoice = object.useLivelyVoice ?? false; message.videoTitle = object.videoTitle ?? ""; return message; } }; function createBaseVideoTranslationResponse() { return { url: void 0, duration: void 0, status: 0, remainingTime: void 0, unknown0: void 0, translationId: "", language: void 0, message: void 0, isLivelyVoice: false, allowToTranslateVideo: void 0, shouldRetry: void 0, unknown3: void 0 }; } var VideoTranslationResponse = { encode(message, writer = new BinaryWriter()) { if (message.url !== void 0) writer.uint32(10).string(message.url); if (message.duration !== void 0) writer.uint32(17).double(message.duration); if (message.status !== 0) writer.uint32(32).int32(message.status); if (message.remainingTime !== void 0) writer.uint32(40).int32(message.remainingTime); if (message.unknown0 !== void 0) writer.uint32(48).int32(message.unknown0); if (message.translationId !== "") writer.uint32(58).string(message.translationId); if (message.language !== void 0) writer.uint32(66).string(message.language); if (message.message !== void 0) writer.uint32(74).string(message.message); if (message.isLivelyVoice !== false) writer.uint32(80).bool(message.isLivelyVoice); if (message.allowToTranslateVideo !== void 0) writer.uint32(88).bool(message.allowToTranslateVideo); if (message.shouldRetry !== void 0) writer.uint32(96).int32(message.shouldRetry); if (message.unknown3 !== void 0) writer.uint32(104).int32(message.unknown3); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 2: if (tag !== 17) break; message.duration = reader.double(); continue; case 4: if (tag !== 32) break; message.status = reader.int32(); continue; case 5: if (tag !== 40) break; message.remainingTime = reader.int32(); continue; case 6: if (tag !== 48) break; message.unknown0 = reader.int32(); continue; case 7: if (tag !== 58) break; message.translationId = reader.string(); continue; case 8: if (tag !== 66) break; message.language = reader.string(); continue; case 9: if (tag !== 74) break; message.message = reader.string(); continue; case 10: if (tag !== 80) break; message.isLivelyVoice = reader.bool(); continue; case 11: if (tag !== 88) break; message.allowToTranslateVideo = reader.bool(); continue; case 12: if (tag !== 96) break; message.shouldRetry = reader.int32(); continue; case 13: if (tag !== 104) break; message.unknown3 = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : void 0, duration: isSet(object.duration) ? globalThis.Number(object.duration) : void 0, status: isSet(object.status) ? globalThis.Number(object.status) : 0, remainingTime: isSet(object.remainingTime) ? globalThis.Number(object.remainingTime) : void 0, unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : void 0, translationId: isSet(object.translationId) ? globalThis.String(object.translationId) : "", language: isSet(object.language) ? globalThis.String(object.language) : void 0, message: isSet(object.message) ? globalThis.String(object.message) : void 0, isLivelyVoice: isSet(object.isLivelyVoice) ? globalThis.Boolean(object.isLivelyVoice) : false, allowToTranslateVideo: isSet(object.allowToTranslateVideo) ? globalThis.Boolean(object.allowToTranslateVideo) : void 0, shouldRetry: isSet(object.shouldRetry) ? globalThis.Number(object.shouldRetry) : void 0, unknown3: isSet(object.unknown3) ? globalThis.Number(object.unknown3) : void 0 }; }, toJSON(message) { const obj = {}; if (message.url !== void 0) obj.url = message.url; if (message.duration !== void 0) obj.duration = message.duration; if (message.status !== 0) obj.status = Math.round(message.status); if (message.remainingTime !== void 0) obj.remainingTime = Math.round(message.remainingTime); if (message.unknown0 !== void 0) obj.unknown0 = Math.round(message.unknown0); if (message.translationId !== "") obj.translationId = message.translationId; if (message.language !== void 0) obj.language = message.language; if (message.message !== void 0) obj.message = message.message; if (message.isLivelyVoice !== false) obj.isLivelyVoice = message.isLivelyVoice; if (message.allowToTranslateVideo !== void 0) obj.allowToTranslateVideo = message.allowToTranslateVideo; if (message.shouldRetry !== void 0) obj.shouldRetry = Math.round(message.shouldRetry); if (message.unknown3 !== void 0) obj.unknown3 = Math.round(message.unknown3); return obj; }, create(base) { return VideoTranslationResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationResponse(); message.url = object.url ?? void 0; message.duration = object.duration ?? void 0; message.status = object.status ?? 0; message.remainingTime = object.remainingTime ?? void 0; message.unknown0 = object.unknown0 ?? void 0; message.translationId = object.translationId ?? ""; message.language = object.language ?? void 0; message.message = object.message ?? void 0; message.isLivelyVoice = object.isLivelyVoice ?? false; message.allowToTranslateVideo = object.allowToTranslateVideo ?? void 0; message.shouldRetry = object.shouldRetry ?? void 0; message.unknown3 = object.unknown3 ?? void 0; return message; } }; function createBaseVideoTranslationCacheItem() { return { status: 0, remainingTime: void 0, message: void 0, unknown0: void 0, flags: [] }; } var VideoTranslationCacheItem = { encode(message, writer = new BinaryWriter()) { if (message.status !== 0) writer.uint32(8).int32(message.status); if (message.remainingTime !== void 0) writer.uint32(16).int32(message.remainingTime); if (message.message !== void 0) writer.uint32(26).string(message.message); if (message.unknown0 !== void 0) writer.uint32(32).int32(message.unknown0); writer.uint32(42).fork(); for (const v of message.flags) writer.int32(v); writer.join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationCacheItem(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.status = reader.int32(); continue; case 2: if (tag !== 16) break; message.remainingTime = reader.int32(); continue; case 3: if (tag !== 26) break; message.message = reader.string(); continue; case 4: if (tag !== 32) break; message.unknown0 = reader.int32(); continue; case 5: if (tag === 40) { message.flags.push(reader.int32()); continue; } if (tag === 42) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) message.flags.push(reader.int32()); continue; } } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { status: isSet(object.status) ? globalThis.Number(object.status) : 0, remainingTime: isSet(object.remainingTime) ? globalThis.Number(object.remainingTime) : void 0, message: isSet(object.message) ? globalThis.String(object.message) : void 0, unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : void 0, flags: globalThis.Array.isArray(object?.flags) ? object.flags.map((e) => globalThis.Number(e)) : [] }; }, toJSON(message) { const obj = {}; if (message.status !== 0) obj.status = Math.round(message.status); if (message.remainingTime !== void 0) obj.remainingTime = Math.round(message.remainingTime); if (message.message !== void 0) obj.message = message.message; if (message.unknown0 !== void 0) obj.unknown0 = Math.round(message.unknown0); if (message.flags?.length) obj.flags = message.flags.map((e) => Math.round(e)); return obj; }, create(base) { return VideoTranslationCacheItem.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationCacheItem(); message.status = object.status ?? 0; message.remainingTime = object.remainingTime ?? void 0; message.message = object.message ?? void 0; message.unknown0 = object.unknown0 ?? void 0; message.flags = object.flags?.map((e) => e) || []; return message; } }; function createBaseVideoTranslationCacheRequest() { return { url: "", duration: 0, language: "", responseLanguage: "" }; } var VideoTranslationCacheRequest = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(10).string(message.url); if (message.duration !== 0) writer.uint32(17).double(message.duration); if (message.language !== "") writer.uint32(26).string(message.language); if (message.responseLanguage !== "") writer.uint32(34).string(message.responseLanguage); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationCacheRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 2: if (tag !== 17) break; message.duration = reader.double(); continue; case 3: if (tag !== 26) break; message.language = reader.string(); continue; case 4: if (tag !== 34) break; message.responseLanguage = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", duration: isSet(object.duration) ? globalThis.Number(object.duration) : 0, language: isSet(object.language) ? globalThis.String(object.language) : "", responseLanguage: isSet(object.responseLanguage) ? globalThis.String(object.responseLanguage) : "" }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.duration !== 0) obj.duration = message.duration; if (message.language !== "") obj.language = message.language; if (message.responseLanguage !== "") obj.responseLanguage = message.responseLanguage; return obj; }, create(base) { return VideoTranslationCacheRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationCacheRequest(); message.url = object.url ?? ""; message.duration = object.duration ?? 0; message.language = object.language ?? ""; message.responseLanguage = object.responseLanguage ?? ""; return message; } }; function createBaseVideoTranslationCacheResponse() { return { default: void 0, cloning: void 0 }; } var VideoTranslationCacheResponse = { encode(message, writer = new BinaryWriter()) { if (message.default !== void 0) VideoTranslationCacheItem.encode(message.default, writer.uint32(10).fork()).join(); if (message.cloning !== void 0) VideoTranslationCacheItem.encode(message.cloning, writer.uint32(18).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationCacheResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.default = VideoTranslationCacheItem.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) break; message.cloning = VideoTranslationCacheItem.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { default: isSet(object.default) ? VideoTranslationCacheItem.fromJSON(object.default) : void 0, cloning: isSet(object.cloning) ? VideoTranslationCacheItem.fromJSON(object.cloning) : void 0 }; }, toJSON(message) { const obj = {}; if (message.default !== void 0) obj.default = VideoTranslationCacheItem.toJSON(message.default); if (message.cloning !== void 0) obj.cloning = VideoTranslationCacheItem.toJSON(message.cloning); return obj; }, create(base) { return VideoTranslationCacheResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationCacheResponse(); message.default = object.default !== void 0 && object.default !== null ? VideoTranslationCacheItem.fromPartial(object.default) : void 0; message.cloning = object.cloning !== void 0 && object.cloning !== null ? VideoTranslationCacheItem.fromPartial(object.cloning) : void 0; return message; } }; function createBaseVideoLangCacheRequest() { return { url: "", title: "" }; } var VideoLangCacheRequest = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(10).string(message.url); if (message.title !== "") writer.uint32(18).string(message.title); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoLangCacheRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 2: if (tag !== 18) break; message.title = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", title: isSet(object.title) ? globalThis.String(object.title) : "" }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.title !== "") obj.title = message.title; return obj; }, create(base) { return VideoLangCacheRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoLangCacheRequest(); message.url = object.url ?? ""; message.title = object.title ?? ""; return message; } }; function createBaseVideoLangCacheResponse() { return { url: "", status: 0, unknown1: 0 }; } var VideoLangCacheResponse = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(10).string(message.url); if (message.status !== 0) writer.uint32(24).int32(message.status); if (message.unknown1 !== 0) writer.uint32(40).int32(message.unknown1); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoLangCacheResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 3: if (tag !== 24) break; message.status = reader.int32(); continue; case 5: if (tag !== 40) break; message.unknown1 = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", status: isSet(object.status) ? globalThis.Number(object.status) : 0, unknown1: isSet(object.unknown1) ? globalThis.Number(object.unknown1) : 0 }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.status !== 0) obj.status = Math.round(message.status); if (message.unknown1 !== 0) obj.unknown1 = Math.round(message.unknown1); return obj; }, create(base) { return VideoLangCacheResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoLangCacheResponse(); message.url = object.url ?? ""; message.status = object.status ?? 0; message.unknown1 = object.unknown1 ?? 0; return message; } }; function createBaseAudioBufferObject() { return { audioFile: /* @__PURE__ */ new Uint8Array(0), fileId: "" }; } var AudioBufferObject = { encode(message, writer = new BinaryWriter()) { if (message.audioFile.length !== 0) writer.uint32(18).bytes(message.audioFile); if (message.fileId !== "") writer.uint32(10).string(message.fileId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseAudioBufferObject(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 2: if (tag !== 18) break; message.audioFile = reader.bytes(); continue; case 1: if (tag !== 10) break; message.fileId = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { audioFile: isSet(object.audioFile) ? bytesFromBase64(object.audioFile) : /* @__PURE__ */ new Uint8Array(0), fileId: isSet(object.fileId) ? globalThis.String(object.fileId) : "" }; }, toJSON(message) { const obj = {}; if (message.audioFile.length !== 0) obj.audioFile = base64FromBytes(message.audioFile); if (message.fileId !== "") obj.fileId = message.fileId; return obj; }, create(base) { return AudioBufferObject.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseAudioBufferObject(); message.audioFile = object.audioFile ?? /* @__PURE__ */ new Uint8Array(0); message.fileId = object.fileId ?? ""; return message; } }; function createBasePartialAudioBufferObject() { return { audioFile: /* @__PURE__ */ new Uint8Array(0), chunkId: 0 }; } var PartialAudioBufferObject = { encode(message, writer = new BinaryWriter()) { if (message.audioFile.length !== 0) writer.uint32(18).bytes(message.audioFile); if (message.chunkId !== 0) writer.uint32(8).int32(message.chunkId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBasePartialAudioBufferObject(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 2: if (tag !== 18) break; message.audioFile = reader.bytes(); continue; case 1: if (tag !== 8) break; message.chunkId = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { audioFile: isSet(object.audioFile) ? bytesFromBase64(object.audioFile) : /* @__PURE__ */ new Uint8Array(0), chunkId: isSet(object.chunkId) ? globalThis.Number(object.chunkId) : 0 }; }, toJSON(message) { const obj = {}; if (message.audioFile.length !== 0) obj.audioFile = base64FromBytes(message.audioFile); if (message.chunkId !== 0) obj.chunkId = Math.round(message.chunkId); return obj; }, create(base) { return PartialAudioBufferObject.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBasePartialAudioBufferObject(); message.audioFile = object.audioFile ?? /* @__PURE__ */ new Uint8Array(0); message.chunkId = object.chunkId ?? 0; return message; } }; function createBaseChunkAudioObject() { return { audioBuffer: void 0, audioPartsLength: 0, fileId: "", version: 0 }; } var ChunkAudioObject = { encode(message, writer = new BinaryWriter()) { if (message.audioBuffer !== void 0) PartialAudioBufferObject.encode(message.audioBuffer, writer.uint32(10).fork()).join(); if (message.audioPartsLength !== 0) writer.uint32(16).int32(message.audioPartsLength); if (message.fileId !== "") writer.uint32(26).string(message.fileId); if (message.version !== 0) writer.uint32(32).int32(message.version); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseChunkAudioObject(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.audioBuffer = PartialAudioBufferObject.decode(reader, reader.uint32()); continue; case 2: if (tag !== 16) break; message.audioPartsLength = reader.int32(); continue; case 3: if (tag !== 26) break; message.fileId = reader.string(); continue; case 4: if (tag !== 32) break; message.version = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { audioBuffer: isSet(object.audioBuffer) ? PartialAudioBufferObject.fromJSON(object.audioBuffer) : void 0, audioPartsLength: isSet(object.audioPartsLength) ? globalThis.Number(object.audioPartsLength) : 0, fileId: isSet(object.fileId) ? globalThis.String(object.fileId) : "", version: isSet(object.version) ? globalThis.Number(object.version) : 0 }; }, toJSON(message) { const obj = {}; if (message.audioBuffer !== void 0) obj.audioBuffer = PartialAudioBufferObject.toJSON(message.audioBuffer); if (message.audioPartsLength !== 0) obj.audioPartsLength = Math.round(message.audioPartsLength); if (message.fileId !== "") obj.fileId = message.fileId; if (message.version !== 0) obj.version = Math.round(message.version); return obj; }, create(base) { return ChunkAudioObject.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseChunkAudioObject(); message.audioBuffer = object.audioBuffer !== void 0 && object.audioBuffer !== null ? PartialAudioBufferObject.fromPartial(object.audioBuffer) : void 0; message.audioPartsLength = object.audioPartsLength ?? 0; message.fileId = object.fileId ?? ""; message.version = object.version ?? 0; return message; } }; function createBaseVideoTranslationAudioRequest() { return { translationId: "", url: "", partialAudioInfo: void 0, audioInfo: void 0 }; } var VideoTranslationAudioRequest = { encode(message, writer = new BinaryWriter()) { if (message.translationId !== "") writer.uint32(10).string(message.translationId); if (message.url !== "") writer.uint32(18).string(message.url); if (message.partialAudioInfo !== void 0) ChunkAudioObject.encode(message.partialAudioInfo, writer.uint32(34).fork()).join(); if (message.audioInfo !== void 0) AudioBufferObject.encode(message.audioInfo, writer.uint32(50).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationAudioRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.translationId = reader.string(); continue; case 2: if (tag !== 18) break; message.url = reader.string(); continue; case 4: if (tag !== 34) break; message.partialAudioInfo = ChunkAudioObject.decode(reader, reader.uint32()); continue; case 6: if (tag !== 50) break; message.audioInfo = AudioBufferObject.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { translationId: isSet(object.translationId) ? globalThis.String(object.translationId) : "", url: isSet(object.url) ? globalThis.String(object.url) : "", partialAudioInfo: isSet(object.partialAudioInfo) ? ChunkAudioObject.fromJSON(object.partialAudioInfo) : void 0, audioInfo: isSet(object.audioInfo) ? AudioBufferObject.fromJSON(object.audioInfo) : void 0 }; }, toJSON(message) { const obj = {}; if (message.translationId !== "") obj.translationId = message.translationId; if (message.url !== "") obj.url = message.url; if (message.partialAudioInfo !== void 0) obj.partialAudioInfo = ChunkAudioObject.toJSON(message.partialAudioInfo); if (message.audioInfo !== void 0) obj.audioInfo = AudioBufferObject.toJSON(message.audioInfo); return obj; }, create(base) { return VideoTranslationAudioRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationAudioRequest(); message.translationId = object.translationId ?? ""; message.url = object.url ?? ""; message.partialAudioInfo = object.partialAudioInfo !== void 0 && object.partialAudioInfo !== null ? ChunkAudioObject.fromPartial(object.partialAudioInfo) : void 0; message.audioInfo = object.audioInfo !== void 0 && object.audioInfo !== null ? AudioBufferObject.fromPartial(object.audioInfo) : void 0; return message; } }; function createBaseVideoTranslationAudioResponse() { return { status: 0, remainingChunks: [] }; } var VideoTranslationAudioResponse = { encode(message, writer = new BinaryWriter()) { if (message.status !== 0) writer.uint32(8).int32(message.status); for (const v of message.remainingChunks) writer.uint32(18).string(v); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoTranslationAudioResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.status = reader.int32(); continue; case 2: if (tag !== 18) break; message.remainingChunks.push(reader.string()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { status: isSet(object.status) ? globalThis.Number(object.status) : 0, remainingChunks: globalThis.Array.isArray(object?.remainingChunks) ? object.remainingChunks.map((e) => globalThis.String(e)) : [] }; }, toJSON(message) { const obj = {}; if (message.status !== 0) obj.status = Math.round(message.status); if (message.remainingChunks?.length) obj.remainingChunks = message.remainingChunks; return obj; }, create(base) { return VideoTranslationAudioResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseVideoTranslationAudioResponse(); message.status = object.status ?? 0; message.remainingChunks = object.remainingChunks?.map((e) => e) || []; return message; } }; function createBaseSubtitlesObject() { return { language: "", url: "", hasTranslation: false, translatedLanguage: "", translatedUrl: "", unknown1: false, subtitleId: 0 }; } var SubtitlesObject = { encode(message, writer = new BinaryWriter()) { if (message.language !== "") writer.uint32(10).string(message.language); if (message.url !== "") writer.uint32(18).string(message.url); if (message.hasTranslation !== false) writer.uint32(24).bool(message.hasTranslation); if (message.translatedLanguage !== "") writer.uint32(34).string(message.translatedLanguage); if (message.translatedUrl !== "") writer.uint32(42).string(message.translatedUrl); if (message.unknown1 !== false) writer.uint32(48).bool(message.unknown1); if (message.subtitleId !== 0) writer.uint32(56).uint32(message.subtitleId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSubtitlesObject(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.language = reader.string(); continue; case 2: if (tag !== 18) break; message.url = reader.string(); continue; case 3: if (tag !== 24) break; message.hasTranslation = reader.bool(); continue; case 4: if (tag !== 34) break; message.translatedLanguage = reader.string(); continue; case 5: if (tag !== 42) break; message.translatedUrl = reader.string(); continue; case 6: if (tag !== 48) break; message.unknown1 = reader.bool(); continue; case 7: if (tag !== 56) break; message.subtitleId = reader.uint32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { language: isSet(object.language) ? globalThis.String(object.language) : "", url: isSet(object.url) ? globalThis.String(object.url) : "", hasTranslation: isSet(object.hasTranslation) ? globalThis.Boolean(object.hasTranslation) : false, translatedLanguage: isSet(object.translatedLanguage) ? globalThis.String(object.translatedLanguage) : "", translatedUrl: isSet(object.translatedUrl) ? globalThis.String(object.translatedUrl) : "", unknown1: isSet(object.unknown1) ? globalThis.Boolean(object.unknown1) : false, subtitleId: isSet(object.subtitleId) ? globalThis.Number(object.subtitleId) : 0 }; }, toJSON(message) { const obj = {}; if (message.language !== "") obj.language = message.language; if (message.url !== "") obj.url = message.url; if (message.hasTranslation !== false) obj.hasTranslation = message.hasTranslation; if (message.translatedLanguage !== "") obj.translatedLanguage = message.translatedLanguage; if (message.translatedUrl !== "") obj.translatedUrl = message.translatedUrl; if (message.unknown1 !== false) obj.unknown1 = message.unknown1; if (message.subtitleId !== 0) obj.subtitleId = Math.round(message.subtitleId); return obj; }, create(base) { return SubtitlesObject.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseSubtitlesObject(); message.language = object.language ?? ""; message.url = object.url ?? ""; message.hasTranslation = object.hasTranslation ?? false; message.translatedLanguage = object.translatedLanguage ?? ""; message.translatedUrl = object.translatedUrl ?? ""; message.unknown1 = object.unknown1 ?? false; message.subtitleId = object.subtitleId ?? 0; return message; } }; function createBaseSubtitlesRequest() { return { url: "", language: "" }; } var SubtitlesRequest = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(10).string(message.url); if (message.language !== "") writer.uint32(18).string(message.language); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSubtitlesRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 2: if (tag !== 18) break; message.language = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", language: isSet(object.language) ? globalThis.String(object.language) : "" }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.language !== "") obj.language = message.language; return obj; }, create(base) { return SubtitlesRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseSubtitlesRequest(); message.url = object.url ?? ""; message.language = object.language ?? ""; return message; } }; function createBaseSubtitlesResponse() { return { waiting: false, subtitles: [] }; } var SubtitlesResponse = { encode(message, writer = new BinaryWriter()) { if (message.waiting !== false) writer.uint32(8).bool(message.waiting); for (const v of message.subtitles) SubtitlesObject.encode(v, writer.uint32(18).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSubtitlesResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.waiting = reader.bool(); continue; case 2: if (tag !== 18) break; message.subtitles.push(SubtitlesObject.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { waiting: isSet(object.waiting) ? globalThis.Boolean(object.waiting) : false, subtitles: globalThis.Array.isArray(object?.subtitles) ? object.subtitles.map((e) => SubtitlesObject.fromJSON(e)) : [] }; }, toJSON(message) { const obj = {}; if (message.waiting !== false) obj.waiting = message.waiting; if (message.subtitles?.length) obj.subtitles = message.subtitles.map((e) => SubtitlesObject.toJSON(e)); return obj; }, create(base) { return SubtitlesResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseSubtitlesResponse(); message.waiting = object.waiting ?? false; message.subtitles = object.subtitles?.map((e) => SubtitlesObject.fromPartial(e)) || []; return message; } }; function createBaseStreamTranslationObject() { return { url: "", timestamp: "" }; } var StreamTranslationObject = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(10).string(message.url); if (message.timestamp !== "") writer.uint32(18).string(message.timestamp); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamTranslationObject(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 2: if (tag !== 18) break; message.timestamp = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", timestamp: isSet(object.timestamp) ? globalThis.String(object.timestamp) : "" }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.timestamp !== "") obj.timestamp = message.timestamp; return obj; }, create(base) { return StreamTranslationObject.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseStreamTranslationObject(); message.url = object.url ?? ""; message.timestamp = object.timestamp ?? ""; return message; } }; function createBaseStreamTranslationRequest() { return { url: "", language: "", responseLanguage: "", unknown0: 0, unknown1: 0 }; } var StreamTranslationRequest = { encode(message, writer = new BinaryWriter()) { if (message.url !== "") writer.uint32(10).string(message.url); if (message.language !== "") writer.uint32(18).string(message.language); if (message.responseLanguage !== "") writer.uint32(26).string(message.responseLanguage); if (message.unknown0 !== 0) writer.uint32(40).int32(message.unknown0); if (message.unknown1 !== 0) writer.uint32(48).int32(message.unknown1); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamTranslationRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; case 2: if (tag !== 18) break; message.language = reader.string(); continue; case 3: if (tag !== 26) break; message.responseLanguage = reader.string(); continue; case 5: if (tag !== 40) break; message.unknown0 = reader.int32(); continue; case 6: if (tag !== 48) break; message.unknown1 = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { url: isSet(object.url) ? globalThis.String(object.url) : "", language: isSet(object.language) ? globalThis.String(object.language) : "", responseLanguage: isSet(object.responseLanguage) ? globalThis.String(object.responseLanguage) : "", unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : 0, unknown1: isSet(object.unknown1) ? globalThis.Number(object.unknown1) : 0 }; }, toJSON(message) { const obj = {}; if (message.url !== "") obj.url = message.url; if (message.language !== "") obj.language = message.language; if (message.responseLanguage !== "") obj.responseLanguage = message.responseLanguage; if (message.unknown0 !== 0) obj.unknown0 = Math.round(message.unknown0); if (message.unknown1 !== 0) obj.unknown1 = Math.round(message.unknown1); return obj; }, create(base) { return StreamTranslationRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseStreamTranslationRequest(); message.url = object.url ?? ""; message.language = object.language ?? ""; message.responseLanguage = object.responseLanguage ?? ""; message.unknown0 = object.unknown0 ?? 0; message.unknown1 = object.unknown1 ?? 0; return message; } }; function createBaseStreamTranslationResponse() { return { interval: 0, translatedInfo: void 0, pingId: void 0 }; } var StreamTranslationResponse = { encode(message, writer = new BinaryWriter()) { if (message.interval !== 0) writer.uint32(8).int32(message.interval); if (message.translatedInfo !== void 0) StreamTranslationObject.encode(message.translatedInfo, writer.uint32(18).fork()).join(); if (message.pingId !== void 0) writer.uint32(24).int32(message.pingId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamTranslationResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.interval = reader.int32(); continue; case 2: if (tag !== 18) break; message.translatedInfo = StreamTranslationObject.decode(reader, reader.uint32()); continue; case 3: if (tag !== 24) break; message.pingId = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { interval: isSet(object.interval) ? streamIntervalFromJSON(object.interval) : 0, translatedInfo: isSet(object.translatedInfo) ? StreamTranslationObject.fromJSON(object.translatedInfo) : void 0, pingId: isSet(object.pingId) ? globalThis.Number(object.pingId) : void 0 }; }, toJSON(message) { const obj = {}; if (message.interval !== 0) obj.interval = streamIntervalToJSON(message.interval); if (message.translatedInfo !== void 0) obj.translatedInfo = StreamTranslationObject.toJSON(message.translatedInfo); if (message.pingId !== void 0) obj.pingId = Math.round(message.pingId); return obj; }, create(base) { return StreamTranslationResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseStreamTranslationResponse(); message.interval = object.interval ?? 0; message.translatedInfo = object.translatedInfo !== void 0 && object.translatedInfo !== null ? StreamTranslationObject.fromPartial(object.translatedInfo) : void 0; message.pingId = object.pingId ?? void 0; return message; } }; function createBaseStreamPingRequest() { return { pingId: 0 }; } var StreamPingRequest = { encode(message, writer = new BinaryWriter()) { if (message.pingId !== 0) writer.uint32(8).int32(message.pingId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamPingRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.pingId = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { pingId: isSet(object.pingId) ? globalThis.Number(object.pingId) : 0 }; }, toJSON(message) { const obj = {}; if (message.pingId !== 0) obj.pingId = Math.round(message.pingId); return obj; }, create(base) { return StreamPingRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseStreamPingRequest(); message.pingId = object.pingId ?? 0; return message; } }; function createBaseYandexSessionRequest() { return { uuid: "", module: "" }; } var YandexSessionRequest = { encode(message, writer = new BinaryWriter()) { if (message.uuid !== "") writer.uint32(10).string(message.uuid); if (message.module !== "") writer.uint32(18).string(message.module); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseYandexSessionRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.uuid = reader.string(); continue; case 2: if (tag !== 18) break; message.module = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { uuid: isSet(object.uuid) ? globalThis.String(object.uuid) : "", module: isSet(object.module) ? globalThis.String(object.module) : "" }; }, toJSON(message) { const obj = {}; if (message.uuid !== "") obj.uuid = message.uuid; if (message.module !== "") obj.module = message.module; return obj; }, create(base) { return YandexSessionRequest.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseYandexSessionRequest(); message.uuid = object.uuid ?? ""; message.module = object.module ?? ""; return message; } }; function createBaseYandexSessionResponse() { return { secretKey: "", expires: 0 }; } var YandexSessionResponse = { encode(message, writer = new BinaryWriter()) { if (message.secretKey !== "") writer.uint32(10).string(message.secretKey); if (message.expires !== 0) writer.uint32(16).int32(message.expires); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseYandexSessionResponse(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.secretKey = reader.string(); continue; case 2: if (tag !== 16) break; message.expires = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; }, fromJSON(object) { return { secretKey: isSet(object.secretKey) ? globalThis.String(object.secretKey) : "", expires: isSet(object.expires) ? globalThis.Number(object.expires) : 0 }; }, toJSON(message) { const obj = {}; if (message.secretKey !== "") obj.secretKey = message.secretKey; if (message.expires !== 0) obj.expires = Math.round(message.expires); return obj; }, create(base) { return YandexSessionResponse.fromPartial(base ?? {}); }, fromPartial(object) { const message = createBaseYandexSessionResponse(); message.secretKey = object.secretKey ?? ""; message.expires = object.expires ?? 0; return message; } }; function bytesFromBase64(b64) { if (globalThis.Buffer) return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); else { const bin = globalThis.atob(b64); const arr = new Uint8Array(bin.length); for (let i = 0; i < bin.length; ++i) arr[i] = bin.charCodeAt(i); return arr; } } function base64FromBytes(arr) { if (globalThis.Buffer) return globalThis.Buffer.from(arr).toString("base64"); else { const bin = []; arr.forEach((byte) => { bin.push(globalThis.String.fromCharCode(byte)); }); return globalThis.btoa(bin.join("")); } } function isSet(value) { return value !== null && value !== void 0; } //#endregion //#region node_modules/@vot.js/shared/dist/types/helpers/bannedvideo.js var TypeName; (function(TypeName) { TypeName["Channel"] = "Channel"; TypeName["Video"] = "Video"; })(TypeName || (TypeName = {})); //#endregion //#region node_modules/@vot.js/ext/dist/helpers/loom.js var LoomHelper = class extends BaseHelper { getClientVersion() { if (typeof SENTRY_RELEASE === "undefined") return; return SENTRY_RELEASE.id; } async getVideoData(videoId) { try { const clientVer = this.getClientVersion(); if (!clientVer) throw new VideoHelperError("Failed to get client version"); const res = await this.fetch("https://www.loom.com/graphql", { headers: { "User-Agent": config_default$1.userAgent, "content-type": "application/json", "x-loom-request-source": `loom_web_${clientVer}`, "apollographql-client-name": "web", "apollographql-client-version": clientVer, "Alt-Used": "www.loom.com" }, body: `{"operationName":"FetchCaptions","variables":{"videoId":"${videoId}"},"query":"query FetchCaptions($videoId: ID!, $password: String) {\\n fetchVideoTranscript(videoId: $videoId, password: $password) {\\n ... on VideoTranscriptDetails {\\n id\\n captions_source_url\\n language\\n __typename\\n }\\n ... on GenericError {\\n message\\n __typename\\n }\\n __typename\\n }\\n}"}`, method: "POST" }); if (res.status !== 200) throw new VideoHelperError("Failed to get data from graphql"); const data = (await res.json()).data.fetchVideoTranscript; if (data.__typename === "GenericError") throw new VideoHelperError(data.message); return { url: this.service?.url + videoId, subtitles: [{ format: "vtt", language: normalizeLang$1(data.language), source: "loom", url: data.captions_source_url }] }; } catch (err) { Logger$1.error(`Failed to get Loom video data, because: ${err.message}`); return this.returnBaseData(videoId); } } async getVideoId(url) { return /(embed|share)\/([^/]+)?/.exec(url.pathname)?.[2]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/mailru.js var MailRuHelper = class extends BaseHelper { API_ORIGIN = "https://my.mail.ru"; async getVideoMeta(videoId) { try { return await (await this.fetch(`${this.API_ORIGIN}/+/video/meta/${videoId}?xemail=&ajax_call=1&func_name=&mna=&mnb=&ext=1&_=${Date.now()}`)).json(); } catch (err) { Logger$1.error("Failed to get mail.ru video data", err.message); return; } } async getVideoId(url) { const pathname = url.pathname; if (/\/(v|mail|bk|inbox)\//.exec(pathname)) return pathname.slice(1); const videoId = /video\/embed\/([^/]+)/.exec(pathname)?.[1]; if (!videoId) return; const videoData = await this.getVideoMeta(videoId); if (!videoData) return; return videoData.meta.url.replace("//my.mail.ru/", ""); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/mediafile.js var MediafileHelper = class extends BaseHelper { DEFAULT_SITE_ORIGIN = "https://mediafile.cc"; SITE_ORIGIN = this.service?.url?.slice(0, -1) ?? this.DEFAULT_SITE_ORIGIN; getVideoSrc() { const video = this.video instanceof HTMLVideoElement ? this.video : document.querySelector("video"); return video?.src || video?.currentSrc || video?.querySelector("source[src]")?.src || void 0; } async getVideoData(videoId) { const videoSource = this.getVideoSrc(); if (!videoSource) return; return { url: `${this.SITE_ORIGIN}/${videoId}`, video_url: videoSource, translationHelp: [{ target: "video_file_url", targetUrl: videoSource }] }; } async getVideoId(url) { return url.pathname.replace(/^\/+/, "") || void 0; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/netacad.js var NetacadHelper = class extends VideoJSHelper { SUBTITLE_SOURCE = "netacad"; async getVideoData(videoId) { const data = this.getVideoDataByPlayer(videoId, true); if (!data) return; return data; } async getVideoId(url) { return url.pathname + url.search; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/newgrounds.js var NewgroundsHelper = class extends BaseHelper { async getVideoId(url) { return /([^/]+)\/(view)\/([^/]+)/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/niconico.js var NicoNicoHelper = class extends BaseHelper { async getVideoId(url) { if (url.hostname === "nico.ms") return url.pathname.replace(/^\//, "").split("/")[0] || void 0; return /\/watch\/([^/?#]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/nine_gag.js var NineGAGHelper = class extends BaseHelper { async getVideoData(videoId) { const data = this.returnBaseData(videoId); if (!data) return data; try { if (!this.video) throw new Error("Video element not found"); const videoUrl = this.video.querySelector("source[type^=\"video/mp4\"], source[type^=\"video/webm\"]")?.src; if (!videoUrl || !/^https?:\/\//.test(videoUrl)) throw new Error("Video source not found"); return { ...data, translationHelp: [{ target: "video_file_url", targetUrl: videoUrl }] }; } catch { return data; } } async getVideoId(url) { return /gag\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/players/jwplayer.js var JWPlayerHelper = class { SUBTITLE_SOURCE = "jwplayer"; SUBTITLE_FORMAT = "vtt"; getHeight(source) { if (typeof source.height === "number" && source.height > 0) return source.height; const match = source.label?.match(/^(\d+)/); return match ? Number.parseInt(match[1], 10) : 0; } getPlayer() { if (typeof jwplayer === "undefined") return; const playerEl = document.querySelector(".jwplayer"); if (playerEl?.id) try { const player = jwplayer(playerEl.id); if (player && typeof player.getPlaylistItem === "function") return player; } catch {} try { return jwplayer(); } catch { return; } } getVideoData(videoId) { try { const player = this.getPlayer(); if (!player) throw new Error("JW Player instance not ready or not found"); let item = player.getPlaylistItem ? player.getPlaylistItem() : null; if (!item && typeof player.getPlaylist === "function") { const playlist = player.getPlaylist(); const index = typeof player.getPlaylistIndex === "function" ? player.getPlaylistIndex() : 0; if (Array.isArray(playlist) && playlist[index]) item = playlist[index]; } if (!item) throw new Error("No playlist item found"); let duration = 0; if (typeof player.getDuration === "function") duration = player.getDuration(); if ((typeof duration !== "number" || duration <= 0) && typeof item.duration === "number") duration = item.duration; const sources = []; if (Array.isArray(item.allSources)) sources.push(...item.allSources); if (Array.isArray(item.sources)) { for (const s of item.sources) if (s?.file && !sources.some((existing) => existing.file === s.file)) sources.push(s); } const validSources = sources.filter((s) => s && typeof s.file === "string" && s.file.length > 0); if (validSources.length === 0) if (typeof item.file === "string" && item.file.length > 0) validSources.push({ file: item.file, type: "", label: "default" }); else throw new Error("No valid sources found"); validSources.sort((a, b) => { const heightA = this.getHeight(a); const heightB = this.getHeight(b); if (heightA === 0 && heightB > 0) return 1; if (heightB === 0 && heightA > 0) return -1; return heightA - heightB; }); const lowestQuality = validSources[0]; return { url: videoId, duration, translationHelp: [{ target: "video_file_url", targetUrl: lowestQuality.file }], subtitles: this.getSubtitles() }; } catch (err) { console.error("[VOT] JWPlayerHelper error:", err instanceof Error ? err.message : String(err)); return; } } getSubtitles() { const subtitles = []; try { const player = this.getPlayer(); if (!player) return subtitles; let item = player.getPlaylistItem ? player.getPlaylistItem() : null; if (!item && typeof player.getPlaylist === "function") { const playlist = player.getPlaylist(); const index = typeof player.getPlaylistIndex === "function" ? player.getPlaylistIndex() : 0; if (Array.isArray(playlist) && playlist[index]) item = playlist[index]; } const tracks = item?.tracks ?? []; const captionsList = typeof player.getCaptionsList === "function" ? player.getCaptionsList() : []; const seenUrls = /* @__PURE__ */ new Set(); const addSubtitle = (label, file) => { if (!file) return; try { const absoluteUrl = new URL(file, window.location.href).toString(); if (!seenUrls.has(absoluteUrl)) { seenUrls.add(absoluteUrl); subtitles.push({ source: this.SUBTITLE_SOURCE, format: this.SUBTITLE_FORMAT, language: normalizeLang$1(label || "en"), url: absoluteUrl }); } } catch {} }; if (Array.isArray(tracks)) { for (const track of tracks) if (track && (track.kind === "captions" || track.kind === "subtitles") && track.file) addSubtitle(track.label || "en", track.file); } if (Array.isArray(captionsList)) { for (const track of captionsList) if (track && typeof track === "object" && track.file) addSubtitle(track.label || "en", track.file); } } catch (err) { console.error("[VOT] JWPlayerHelper getSubtitles error:", err); } return subtitles; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/noodlemagazine.js var NoodleMagazineHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname.slice(1); } async getVideoData(videoId) { return new JWPlayerHelper().getVideoData(videoId); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/odysee.js var OdyseeHelper = class extends BaseHelper { API_ORIGIN = "https://odysee.com"; async getVideoData(videoId) { return { url: `${this.API_ORIGIN}/${videoId}`.replace(/:[a-zA-Z0-9]+$/, "") }; } async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/okru.js var OKRuHelper = class extends BaseHelper { async getVideoId(url) { return /\/video\/(\d+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/olympicsreplay.js var OlympicsReplayHelper = class extends BaseHelper { async getVideoId(url) { return /\/([a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+)\/?$/i.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/oraclelearn.js var OracleLearnHelper = class extends VideoJSHelper { SUBTITLE_SOURCE = "oraclelearn"; async getVideoData(videoId) { const data = this.getVideoDataByPlayer(videoId); if (!data) return; const { url, duration, subtitles } = data; const baseData = this.returnBaseData(videoId); const videoUrl = proxyMedia(new URL(url)); if (!baseData) return { url: videoUrl, duration, subtitles }; return { url: baseData.url, duration, subtitles, translationHelp: [{ target: "video_file_url", targetUrl: videoUrl }] }; } async getVideoId(url) { return /\/ou\/course\/(([^/]+)\/(\d+)\/(\d+))/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/patreon.js var PatreonHelper = class extends BaseHelper { API_ORIGIN = "https://www.patreon.com/api"; async getPosts(postId) { try { return await (await this.fetch(`${this.API_ORIGIN}/posts/${postId}?json-api-use-default-includes=false`)).json(); } catch (err) { Logger$1.error(`Failed to get patreon posts by postId: ${postId}.`, err.message); return false; } } async getVideoData(postId) { const postData = await this.getPosts(postId); if (!postData) return; const postFileUrl = postData.data.attributes.post_file.url; if (!postFileUrl) return; return { url: postFileUrl }; } async getVideoId(url) { const fullPostId = /posts\/([^/]+)/.exec(url.pathname)?.[1]; if (!fullPostId) return; return fullPostId.replace(/[^\d.]/g, ""); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/peertube.js var PeertubeHelper = class extends BaseHelper { async getVideoId(url) { const normalizedPathname = url.pathname.replace(/\/+$/, ""); const watchVideoId = /\/videos\/watch\/([^/]+)/.exec(normalizedPathname)?.[1]; if (watchVideoId) return `/videos/watch/${watchVideoId}`; const shortVideoId = /\/w\/([^/]+)/.exec(normalizedPathname)?.[1]; if (shortVideoId) return `/videos/watch/${shortVideoId}`; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/picarto.js var PicartoHelper = class extends BaseHelper { async getVideoId(url) { return /\/((?:videopopout|[^/]+(?:\/profile)?\/videos)\/[^/?#&/]+)\/?$/.exec(url.pathname)?.[1] ?? /^\/([^/#?]+)\/?$/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/pornhub.js var PornhubHelper = class extends BaseHelper { async getVideoId(url) { return url.searchParams.get("viewkey") ?? /embed\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/porntn.js var PornTNHelper = class extends BaseHelper { async getVideoData(videoId) { try { if (typeof flashvars === "undefined") return; const { rnd, video_url: source, video_title: title } = flashvars; if (!source || !rnd) throw new VideoHelperError("Failed to find video source or rnd"); const getFileUrl = new URL(source); getFileUrl.searchParams.append("rnd", rnd); Logger$1.log("PornTN get_file link", getFileUrl.href); const cdnResponse = await this.fetch(getFileUrl.href, { method: "head" }); const cdnUrl = new URL(cdnResponse.url); Logger$1.log("PornTN cdn link", cdnUrl.href); return { url: proxyMedia(cdnUrl), title }; } catch (err) { Logger$1.error(`Failed to get PornTN data by videoId: ${videoId}`, err.message); return; } } async getVideoId(url) { return /\/videos\/(([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/preservetube.js var PreserveTubeHelper = class extends BaseHelper { async getVideoData(videoId) { try { if (!videoId) throw new VideoHelperError("Failed to find PreserveTube video ID"); return { url: `https://s3.archive.party/preservetube/${videoId}.mp4` }; } catch (err) { Logger$1.error(`Failed to get PreserveTube data by videoId: ${videoId}`, err.message); return; } } async getVideoId(url) { return url.searchParams.get("v") ?? void 0; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/reddit.js var RedditHelper = class extends BaseHelper { API_ORIGIN = "https://www.reddit.com"; async getDashAudioUrl(dashUrl) { const res = await fetch(dashUrl); if (!res.ok) return void 0; const xml = await res.text(); const doc = new DOMParser().parseFromString(xml, "application/xml"); const audioBaseUrl = doc.querySelector("AdaptationSet[contentType=\"audio\"] BaseURL")?.textContent ?? doc.querySelector("Representation[id=\"AUDIO-1\"] BaseURL")?.textContent; if (!audioBaseUrl) return void 0; const base = new URL(dashUrl); return new URL(audioBaseUrl, base).href; } async getVideoData(videoId) { try { const res = await fetch(`${this.API_ORIGIN}/r/${videoId}.json`, { headers: { Accept: "application/json" } }); if (!res.ok) throw new VideoHelperError(`Reddit API error: ${res.status}`); const post = (await res.json())?.[0]?.data?.children?.[0]?.data; const redditVideo = post?.secure_media?.reddit_video ?? post?.media?.reddit_video ?? post?.crosspost_parent_list?.[0]?.secure_media?.reddit_video ?? post?.crosspost_parent_list?.[0]?.media?.reddit_video; if (!redditVideo) throw new VideoHelperError("No reddit_video found in post"); const dashUrl = redditVideo.dash_url?.replaceAll("&", "&"); if (!dashUrl) throw new VideoHelperError("No dash_url in reddit_video"); const audioUrl = await this.getDashAudioUrl(dashUrl); if (!audioUrl) throw new VideoHelperError("Failed to extract audio URL from DASH MPD"); return { url: audioUrl }; } catch (err) { Logger$1.error(`Failed to get reddit video data by video ID: ${videoId}`, err.message); return; } } async getVideoId(url) { return /\/r\/(([^/]+)\/([^/]+)\/([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/rtnews.js var RtNewsHelper = class extends BaseHelper { async getVideoData(videoId) { const videoEl = document.querySelector(".jw-video, .media__video_noscript"); if (!videoEl) return; let videoSrc = videoEl.getAttribute("src"); if (!videoSrc) return; if (videoSrc.endsWith(".MP4")) videoSrc = proxyMedia(videoSrc); return { videoId, url: videoSrc }; } async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/rule34video.js var Rule34VideoHelper = class extends BaseHelper { async getVideoId(url) { const parts = /\/videos?\/(\d+)(?:\/(.+))?\/?$/.exec(url.pathname); if (!parts) return; const [, id, tail] = parts; return tail ? `${id}/${tail.replace(/\/+$/, "")}/` : id; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/rumble.js var RumbleHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/rutube.js var RutubeHelper = class extends BaseHelper { async getVideoId(url) { return /(?:video|embed)\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/sap.js var SapHelper = class extends BaseHelper { API_ORIGIN = "https://learning.sap.com/"; async requestKaltura(kalturaDomain, partnerId, entryId) { const clientTag = "html5:v3.17.22"; const apiVersion = "3.3.0"; try { return await (await this.fetch(`https://${kalturaDomain}/api_v3/service/multirequest`, { method: "POST", body: JSON.stringify({ "1": { service: "session", action: "startWidgetSession", widgetId: `_${partnerId}` }, "2": { service: "baseEntry", action: "list", ks: "{1:result:ks}", filter: { redirectFromEntryId: entryId }, responseProfile: { type: 1, fields: "id,referenceId,name,description,dataUrl,duration,flavorParamsIds,type,dvrStatus,externalSourceType,createdAt,updatedAt,endDate,plays,views,downloadUrl,creatorId" } }, "3": { service: "baseEntry", action: "getPlaybackContext", entryId: "{2:result:objects:0:id}", ks: "{1:result:ks}", contextDataParams: { objectType: "KalturaContextDataParams", flavorTags: "all" } }, apiVersion, format: 1, ks: "", clientTag, partnerId }), headers: { "Content-Type": "application/json" } })).json(); } catch (err) { Logger$1.error("Failed to request kaltura data", err.message); return; } } async getKalturaData(videoId) { const url = `${this.API_ORIGIN}${videoId}`; try { const content = await (await this.fetch(url)).text(); const nextDataEl = new DOMParser().parseFromString(content, "text/html").getElementById("__NEXT_DATA__"); if (!nextDataEl?.textContent) throw new VideoHelperError(`Failed to find __NEXT_DATA__ for ${videoId}`); const video = JSON.parse(nextDataEl.textContent)?.props?.pageProps?.embeddedVideos?.[0]; if (!video) throw new VideoHelperError(`Failed to find embeddedVideos in JSON for ${videoId}`); const contentUrl = video.contentUrl || ""; const match = /https:\/\/([^/]+)\/p\/(\d+)\//i.exec(contentUrl); if (!match) throw new VideoHelperError(`Failed to extract Kaltura domain and partnerId for ${videoId}`); const [, kalturaDomain, partnerId] = match; const entryId = video.videoId; return await this.requestKaltura(kalturaDomain, partnerId, entryId); } catch (err) { Logger$1.error("Failed to get kaltura data", err.message); return; } } async getVideoData(videoId) { const kalturaData = await this.getKalturaData(videoId); if (!kalturaData) return; const [, baseEntryList, playbackContext] = kalturaData; const { duration } = baseEntryList.objects[0]; const videoUrl = playbackContext.sources.find((source) => source.format === "url" && source.protocols === "http,https" && source.url.includes(".mp4"))?.url; if (!videoUrl) return; return { url: videoUrl, subtitles: playbackContext.playbackCaptions?.map((caption) => { return { language: normalizeLang$1(caption.languageCode), source: "sap", format: "vtt", url: caption.webVttUrl, isAutoGenerated: caption.label.includes("auto-generated") }; }) ?? [], duration }; } async getVideoId(url) { return /((courses|learning-journeys)\/([^/]+)(\/[^/]+)?)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/skilljar.js var SkilljarHelper = class extends BaseHelper { async getVideoId(url) { return url.pathname.slice(1); } async getVideoData(videoId) { return new JWPlayerHelper().getVideoData(videoId); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/spankbang.js var SpankBangHelper = class extends BaseHelper { async getVideoId(url) { return /\/([\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?)\/?$/i.exec(url.pathname)?.[1] ?? /\/([\da-z]+-[\da-z]+\/playlist\/[^/]+)\/?$/i.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/telegram.js var TelegramHelper = class TelegramHelper extends BaseHelper { static getMediaViewer() { if (typeof appMediaViewer === "undefined") return; return appMediaViewer; } async getVideoId(_url) { const mediaViewer = TelegramHelper.getMediaViewer(); if (!mediaViewer) return; if (mediaViewer.live) return; const message = mediaViewer.target.message; if (message.peer_id._ !== "peerChannel") return; const media = message.media; if (media._ !== "messageMediaDocument") return; if (media.document.type !== "video") return; const postId = message.mid & 4294967295; return `${await mediaViewer.managers.appPeersManager.getPeerUsername(message.peerId)}/${postId}`; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/thisvid.js var ThisVidHelper = class extends BaseHelper { async getVideoId(url) { return /(videos|embed)\/[^/]+/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/tiktok.js var TikTokHelper = class extends BaseHelper { async getVideoId(url) { return /([^/]+)\/video\/([^/]+)/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/trovo.js var TrovoHelper = class extends BaseHelper { async getVideoId(url) { const vid = url.searchParams.get("vid"); const path = /([^/]+)\/([\d]+)/.exec(url.pathname)?.[0]; if (!vid || !path) return; return `${path}?vid=${vid}`; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/twitch.js var TwitchHelper = class extends BaseHelper { API_ORIGIN = "https://clips.twitch.tv"; async getClipLink(pathname, clipId) { const schema = document.querySelector("script[type='application/ld+json']"); const clearPathname = pathname.slice(1); if (schema) { const channelLink = JSON.parse(schema.innerText)["@graph"].find((obj) => obj["@type"] === "VideoObject")?.creator.url; if (!channelLink) throw new VideoHelperError("Failed to find channel link"); return `${channelLink.replace("https://www.twitch.tv/", "")}/clip/${clearPathname}`; } const isEmbed = clearPathname === "embed"; const channelLink = document.querySelector(isEmbed ? ".tw-link[data-test-selector='stream-info-card-component__stream-avatar-link']" : ".clips-player a:not([class])"); if (!channelLink) return; return `${channelLink.href.replace("https://www.twitch.tv/", "")}/clip/${isEmbed ? clipId : clearPathname}`; } async getVideoData(videoId) { const title = document.querySelector("[data-a-target=\"stream-title\"], [data-test-selector=\"stream-info-card-component__subtitle\"]")?.innerText; const isStream = !!document.querySelector("[data-a-target=\"animated-channel-viewers-count\"], .channel-status-info--live, .top-bar--pointer-enabled .tw-channel-status-text-indicator"); return { url: this.service?.url + videoId, isStream, title }; } async getVideoId(url) { const pathname = url.pathname; if (/^m\.twitch\.tv$/.test(pathname)) return /videos\/([^/]+)/.exec(url.href)?.[0] ?? pathname.slice(1); else if (/^player\.twitch\.tv$/.test(url.hostname)) return `videos/${url.searchParams.get("video")}`; const clipPath = /([^/]+)\/(?:clip)\/([^/]+)/.exec(pathname); if (clipPath) return clipPath[0]; if (/^clips\.twitch\.tv$/.test(url.hostname)) return await this.getClipLink(pathname, url.searchParams.get("clip")); const videoPath = /(?:videos)\/([^/]+)/.exec(pathname); if (videoPath) return videoPath[0]; const isUserOfflinePage = document.querySelector(".home-offline-hero .tw-link"); if (isUserOfflinePage?.href) { const pageUrl = new URL(isUserOfflinePage.href); return /(?:videos)\/([^/]+)/.exec(pageUrl.pathname)?.[0]; } return document.querySelector(".persistent-player") ? pathname : void 0; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/twitter.js var TwitterHelper = class extends BaseHelper { async getVideoId(url) { const videoId = /status\/([^/]+)/.exec(url.pathname)?.[1]; if (videoId) return videoId; const newLink = (this.video?.closest("[data-testid=\"tweet\"]"))?.querySelector("a[role=\"link\"][aria-label]")?.href; return newLink ? /status\/([^/]+)/.exec(newLink)?.[1] : void 0; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/udemy.js function isObject(value) { return typeof value === "object" && value !== null; } function isUrlCandidate(value) { return typeof value === "object" && value !== null; } function getUrlCandidates(data) { if (Array.isArray(data)) return data.filter(isUrlCandidate); if (typeof data !== "object" || data === null) return []; const source = data; return (Array.isArray(source.Video) ? source.Video : Array.isArray(source.video) ? source.video : []).filter(isUrlCandidate); } function getOutputCandidates(data) { if (!isObject(data)) return []; const result = []; for (const [fallbackLabel, value] of Object.entries(data)) { if (!isObject(value) || typeof value.url !== "string") continue; result.push({ src: value.url, type: typeof value.type === "string" ? value.type : void 0, label: typeof value.height === "number" || typeof value.height === "string" ? value.height : fallbackLabel }); } return result; } function getQualityValue(value) { if (typeof value === "number" && Number.isFinite(value)) return value; const match = String(value ?? "").match(/(\d{3,4})/); return Number(match?.[1] ?? 0); } function getCandidateUrl(candidate) { if (typeof candidate.file === "string") return candidate.file; if (typeof candidate.src === "string") return candidate.src; } function isM3U8(type, url) { return type.includes("mpegurl") || /\.m3u8(?:$|[?#])/i.test(url); } function isDash(type, url) { return type.includes("dash") || /\.mpd(?:$|[?#])/i.test(url); } var UdemyHelper = class extends BaseHelper { API_ORIGIN = `${window.location.origin}/api-2.0`; getModuleData() { const moduleData = (document.querySelector(".ud-app-loader[data-module-id='course-taking']") ?? document.querySelector("[data-module-id='course-taking']"))?.dataset?.moduleArgs; if (!moduleData) return; try { return JSON.parse(moduleData); } catch { return; } } getLectureId(videoId) { const lectureIdRe = /(?:\/learn\/(?:v4\/t\/)?lecture\/|#\/?lecture\/)(\d+)/i; const lectureViewIdRe = /\/lecture\/view\/\?/i; const href = window.location.href; const lectureViewSearchParams = lectureViewIdRe.test(href) ? new URL(href).searchParams : void 0; const lectureViewId = lectureViewSearchParams ? (() => { for (const [key, value] of lectureViewSearchParams) { const normalizedKey = key.toLowerCase(); if (normalizedKey === "lectureid" || normalizedKey === "lecture_id") return value; } })() : void 0; return lectureIdRe.exec(href)?.[1] ?? lectureViewId ?? (videoId ? lectureIdRe.exec(`/${videoId}`)?.[1] : void 0); } getCourseId(moduleData) { const moduleDataWithExtra = moduleData; const moduleCourseId = this.normalizeId(moduleDataWithExtra?.courseId ?? moduleDataWithExtra?.course_id ?? moduleDataWithExtra?.course?.id); if (moduleCourseId) return moduleCourseId; const attrCourseId = this.normalizeId(document.querySelector("[data-course-id]")?.getAttribute("data-course-id")); if (attrCourseId) return attrCourseId; const pageHtml = document.documentElement?.innerHTML ?? ""; return /data-course-id=["'](\d+)/i.exec(pageHtml)?.[1] ?? /"courseId"\s*:\s*(\d+)/i.exec(pageHtml)?.[1] ?? /"courseId"\s*:\s*(\d+)/i.exec(pageHtml)?.[1]; } normalizeId(value) { if (typeof value === "number" && Number.isFinite(value)) return String(value); if (typeof value === "string") return /^\d+$/.test(value) ? value : void 0; } parseJson(value) { try { return JSON.parse(value); } catch { const normalized = value.replaceAll(""", "\"").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("'", "'"); try { return JSON.parse(normalized); } catch { return; } } } getViewHtmlCandidates(viewHtml) { if (typeof viewHtml !== "string" || !viewHtml.trim()) return []; const doc = new DOMParser().parseFromString(viewHtml, "text/html"); const candidates = []; for (const sourceEl of Array.from(doc.querySelectorAll("source"))) { const src = sourceEl.getAttribute("src"); if (!src) continue; candidates.push({ src, type: sourceEl.getAttribute("type") ?? void 0, label: sourceEl.getAttribute("data-res") ?? void 0 }); } for (const setupDataEl of Array.from(doc.querySelectorAll("[videojs-setup-data]"))) { const setupDataRaw = setupDataEl.getAttribute("videojs-setup-data"); if (!setupDataRaw) continue; const setupData = this.parseJson(setupDataRaw); if (setupData) candidates.push(...getUrlCandidates(setupData.sources)); } return candidates; } isErrorData(data) { return Object.hasOwn(data, "error") || Object.hasOwn(data, "detail") && !Object.hasOwn(data, "_class"); } async getLectureData(courseId, lectureId) { try { const data = await (await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${courseId}/lectures/${lectureId}/?` + new URLSearchParams({ "fields[lecture]": "title,description,view_html,asset,download_url,is_free,last_watched_second", "fields[asset]": "asset_type,length,stream_url,media_sources,stream_urls,download_urls,external_url,captions,data,thumbnail_sprite,slides,slide_urls,course_is_drmed,media_license_token" }).toString())).json(); if (this.isErrorData(data)) throw new VideoHelperError(data.detail ?? "unknown error"); return data; } catch (err) { Logger$1.error(`Failed to get lecture data by courseId: ${courseId} and lectureId: ${lectureId}`, err.message); return; } } async getCourseLang(courseId) { try { const data = await (await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${courseId}?` + new URLSearchParams({ "fields[course]": "locale" }).toString())).json(); if (!this.isErrorData(data)) return data; const data2 = await (await this.fetch(`${this.API_ORIGIN}/courses/${courseId}/?` + new URLSearchParams({ "fields[course]": "locale" }).toString())).json(); if (this.isErrorData(data2)) throw new VideoHelperError(data2.detail ?? "unknown error"); return data2; } catch (err) { Logger$1.error(`Failed to get course lang by courseId: ${courseId}`, err.message); return; } } findVideoUrl(sources, streamUrls, downloadUrls, streamUrl, externalUrl, outputs, viewHtml) { const allCandidates = []; const mediaSources = Array.isArray(sources) ? sources : []; for (const source of mediaSources) allCandidates.push({ src: source.src, type: source.type, label: source.label }); allCandidates.push(...getUrlCandidates(streamUrls)); allCandidates.push(...getUrlCandidates(downloadUrls)); allCandidates.push(...getOutputCandidates(outputs)); if (typeof viewHtml === "string") allCandidates.push(...this.getViewHtmlCandidates(viewHtml)); if (typeof streamUrl === "string") allCandidates.push({ src: streamUrl }); if (typeof externalUrl === "string") allCandidates.push({ src: externalUrl }); const playerSrc = this.video?.currentSrc || this.video?.src; if (typeof playerSrc === "string" && playerSrc) allCandidates.push({ src: playerSrc }); const dedupCandidates = /* @__PURE__ */ new Map(); for (const candidate of allCandidates) { const url = getCandidateUrl(candidate); if (!url || /^javascript:/i.test(url)) continue; const quality = getQualityValue(candidate.label ?? candidate.quality ?? candidate.height); const type = String(candidate.type ?? "").toLowerCase(); const prev = dedupCandidates.get(url); if (!prev || quality > prev.quality) dedupCandidates.set(url, { url, type, quality, isYouTubeWatch: /:\/\/(?:www\.)?youtube\.com\/watch\?/i.test(url) }); } const candidates = Array.from(dedupCandidates.values()); if (!candidates.length) return; const mp4Candidates = candidates.filter((item) => item.type.includes("mp4") || /\.mp4(?:$|[?#])/i.test(item.url)); if (mp4Candidates.length) { mp4Candidates.sort((a, b) => b.quality - a.quality); return mp4Candidates[0]?.url; } const hlsUrl = candidates.find((item) => isM3U8(item.type, item.url))?.url; if (hlsUrl) return hlsUrl; const dashUrl = candidates.find((item) => isDash(item.type, item.url))?.url; if (dashUrl) return dashUrl; const nonYoutube = candidates.find((item) => !item.isYouTubeWatch)?.url; if (nonYoutube) return nonYoutube; return candidates[0]?.url; } getCaptionLocale(caption) { const localeId = typeof caption.locale_id === "string" ? caption.locale_id : typeof caption.locale?.locale === "string" ? caption.locale.locale : void 0; return localeId ? normalizeLang$1(localeId) : void 0; } findSubtitleUrl(captions, detectedLanguage) { if (!Array.isArray(captions)) return; const captionsWithDownload = captions.filter((caption) => isObject(caption) && (typeof caption.url === "string" || typeof caption.download_url === "string")); const subtitle = captionsWithDownload.find((caption) => this.getCaptionLocale(caption) === detectedLanguage) ?? captionsWithDownload.find((caption) => this.getCaptionLocale(caption) === "en") ?? captionsWithDownload[0]; return subtitle?.url ?? subtitle?.download_url; } async getVideoData(videoId) { const moduleData = this.getModuleData(); const courseId = this.getCourseId(moduleData); const lectureId = this.getLectureId(videoId); Logger$1.log(`[Udemy] courseId: ${courseId}, lectureId: ${lectureId}`); if (!lectureId || !courseId) return; const lectureData = await this.getLectureData(courseId, lectureId); if (!lectureData) return; const { title, description, asset, view_html } = lectureData; const { length: duration, media_sources, captions } = asset; const assetWithExtraUrls = asset; const streamUrls = assetWithExtraUrls.stream_urls; const downloadUrls = assetWithExtraUrls.download_urls; const videoUrl = this.findVideoUrl(media_sources, streamUrls, downloadUrls, assetWithExtraUrls.stream_url ?? assetWithExtraUrls.streamUrl, assetWithExtraUrls.external_url, assetWithExtraUrls.data?.outputs, view_html); if (!videoUrl) { Logger$1.log("Failed to find video file in asset sources", asset); return; } let courseLang = "en"; const courseLocale = (await this.getCourseLang(courseId))?.locale?.locale; if (typeof courseLocale === "string") courseLang = normalizeLang$1(courseLocale); if (!availableLangs.includes(courseLang)) courseLang = "en"; const subtitleUrl = this.findSubtitleUrl(captions, courseLang); if (!subtitleUrl) Logger$1.log("Failed to find subtitle file in captions", captions); return { ...subtitleUrl ? { url: this.service?.url + videoId, translationHelp: [{ target: "subtitles_file_url", targetUrl: subtitleUrl }, { target: "video_file_url", targetUrl: videoUrl }], detectedLanguage: courseLang } : { url: videoUrl, translationHelp: null }, duration, title, description }; } async getVideoId(url) { return url.pathname.slice(1); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/vk.js var VKHelper = class VKHelper extends BaseHelper { static getPlayer() { if (typeof Videoview === "undefined") return; return Videoview?.getPlayerObject?.call(void 0); } async getVideoData(videoId) { const player = VKHelper.getPlayer(); if (!player) return this.returnBaseData(videoId); try { const { description: descriptionHTML, duration, md_title: title } = player.vars; const doc = new DOMParser().parseFromString(descriptionHTML, "text/html"); const description = Array.from(doc.body.childNodes).filter((el) => el.nodeName !== "BR").map((el) => el.textContent).join("\n"); let subtitles; if (Object.hasOwn(player.vars, "subs")) subtitles = player.vars.subs.map((sub) => ({ language: normalizeLang$1(sub.lang), source: "vk", format: "vtt", url: sub.url, isAutoGenerated: !!sub.is_auto })); return { url: this.service?.url + videoId, title, description, duration, subtitles }; } catch (err) { Logger$1.error(`Failed to get VK video data, because: ${err.message}`); return this.returnBaseData(videoId); } } async getVideoId(url) { const pathID = /^\/(video|clip)-?\d{8,9}_\d{9}$/.exec(url.pathname); if (pathID) return pathID[0].slice(1); const idInsidePlaylist = /\/playlist\/[^/]+\/(video-?\d{8,9}_\d{9})/.exec(url.pathname); if (idInsidePlaylist) return idInsidePlaylist[1]; const paramZ = url.searchParams.get("z"); if (paramZ) return paramZ.split("/")[0]; const paramOID = url.searchParams.get("oid"); const paramID = url.searchParams.get("id"); if (paramOID && paramID) return `video-${Math.abs(parseInt(paramOID, 10))}_${paramID}`; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/watchpornto.js var WatchPornToHelper = class extends BaseHelper { async getVideoId(url) { return /(video|embed)\/(\d+)(\/[^/]+\/)?/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/weibo.js var weiboVideoIdRe = /^\d+:(?:[\da-f]{32}|\d{16,})$/i; var weiboLayerIdRe = /^[A-Za-z0-9]+$/; var weiboHostRe = /^(?:www\.)?weibo\.com$/; var weiboLoginPathRe = /^\/newlogin\/?$/; var WeiboHelper = class extends BaseHelper { async getVideoId(url) { if (url.hostname === "video.weibo.com") { const fid = url.searchParams.get("fid"); if (!fid || !weiboVideoIdRe.test(fid)) return; return `tv/show/${fid}`; } if (weiboHostRe.test(url.host) && weiboLoginPathRe.test(url.pathname)) { const nestedUrl = url.searchParams.get("url"); if (nestedUrl) try { const parsedNestedUrl = new URL(nestedUrl, url.origin); if (parsedNestedUrl.href !== url.href) { const nestedVideoId = await this.getVideoId(parsedNestedUrl); if (nestedVideoId) return nestedVideoId; } } catch {} const layerId = url.searchParams.get("layerid"); if (layerId && weiboLayerIdRe.test(layerId)) return `0/${layerId}`; } const normalizedPath = url.pathname.replace(/\/+$/, ""); if (/^\/\d+\/[A-Za-z0-9]+$/.test(normalizedPath) || /^\/0\/[A-Za-z0-9]+$/.test(normalizedPath) || /^\/tv\/show\/\d+:(?:[\da-f]{32}|\d{16,})$/i.test(normalizedPath)) return normalizedPath.slice(1); } }; //#endregion //#region src/shims/nodeCrypto.ts var nodeCrypto_exports = /* @__PURE__ */ __exportAll({ default: () => webCrypto, getRandomValues: () => getRandomValues, randomUUID: () => randomUUID, subtle: () => subtle }); var webCrypto, subtle, getRandomValues, randomUUID; var init_nodeCrypto = __esmMin((() => { webCrypto = globalThis.crypto; if (!webCrypto?.subtle) throw new TypeError("Web Crypto API is not available in this environment."); subtle = webCrypto.subtle; getRandomValues = webCrypto.getRandomValues.bind(webCrypto); randomUUID = typeof webCrypto.randomUUID === "function" ? webCrypto.randomUUID.bind(webCrypto) : void 0; })); //#endregion //#region node_modules/@vot.js/shared/dist/secure.js var { componentVersion, chromiumRevision } = config_default$1; async function getCrypto() { if (typeof window !== "undefined" && window.crypto) return window.crypto; return await Promise.resolve().then(() => (init_nodeCrypto(), nodeCrypto_exports)); } var utf8Encoder = new TextEncoder(); async function signHMAC(hashName, hmac, data) { const crypto = await getCrypto(); const key = await crypto.subtle.importKey("raw", utf8Encoder.encode(hmac), { name: "HMAC", hash: { name: hashName } }, false, ["sign", "verify"]); return await crypto.subtle.sign("HMAC", key, data); } async function getSignature(body) { const signature = await signHMAC("SHA-256", config_default$1.hmac, body); return new Uint8Array(signature).reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); } async function getSecYaHeaders(secType, session, body, path) { const { secretKey, uuid } = session; const token = `${uuid}:${path}:${componentVersion}`; const tokenSign = await getSignature(utf8Encoder.encode(token)); if (secType === "Ya-Summary") return { [`X-${secType}-Sk`]: secretKey, [`X-${secType}-Token`]: `${tokenSign}:${token}` }; if (!body) throw new TypeError(`Body is required for sec type ${secType}`); const sign = await getSignature(body); return { [`${secType}-Signature`]: sign, [`Sec-${secType}-Sk`]: secretKey, [`Sec-${secType}-Token`]: `${tokenSign}:${token}` }; } function getUUID() { const hexDigits = "0123456789ABCDEF"; let uuid = ""; for (let i = 0; i < 32; i++) { const randomDigit = Math.floor(Math.random() * 16); uuid += hexDigits[randomDigit]; } return uuid; } async function getHmacSha1(hmacKey, salt) { try { const signature = await signHMAC("SHA-1", hmacKey, utf8Encoder.encode(salt)); return btoa(String.fromCharCode(...new Uint8Array(signature))); } catch (err) { Logger$1.error(err); return false; } } var browserSecHeaders = { "sec-ch-ua": `"Not;A=Brand";v="8", "Chromium";v="150", "YaBrowser";v="${componentVersion.slice(0, 5)}", "Yowser";v="2.5"`, "sec-ch-ua-full-version-list": `"Not;A=Brand";v="8.0.0.0", "Chromium";v="150.0.7871.${chromiumRevision}", "YaBrowser";v="${componentVersion}", "Yowser";v="2.5"`, "Sec-Fetch-Mode": "no-cors" }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/weverse.js var WeverseHelper = class extends BaseHelper { API_ORIGIN = "https://global.apis.naver.com/weverse/wevweb"; API_APP_ID = "be4d79eb8fc7bd008ee82c8ec4ff6fd4"; API_HMAC_KEY = "1b9cb6378d959b45714bec49971ade22e6e24e42"; HEADERS = { Accept: "application/json, text/plain, */*", Origin: "https://weverse.io", Referer: "https://weverse.io/" }; getURLData() { return { appId: this.API_APP_ID, language: "en", os: "WEB", platform: "WEB", wpf: "pc" }; } async createHash(pathname) { const timestamp = Date.now(); const salt = pathname.substring(0, Math.min(255, pathname.length)) + timestamp; const sign = await getHmacSha1(this.API_HMAC_KEY, salt); if (!sign) throw new VideoHelperError("Failed to get weverse HMAC signature"); return { wmsgpad: timestamp.toString(), wmd: sign }; } async getHashURLParams(pathname) { const hash = await this.createHash(pathname); return new URLSearchParams(hash).toString(); } async getPostPreview(postId) { const pathname = `/post/v1.0/post-${postId}/preview?` + new URLSearchParams({ fieldSet: "postForPreview", ...this.getURLData() }).toString(); try { const urlParams = await this.getHashURLParams(pathname); return await (await this.fetch(`${this.API_ORIGIN + pathname}&${urlParams}`, { headers: this.HEADERS })).json(); } catch (err) { Logger$1.error(`Failed to get weverse post preview by postId: ${postId}`, err.message); return false; } } async getVideoInKey(videoId) { const pathname = `/video/v1.1/vod/${videoId}/inKey?` + new URLSearchParams({ gcc: "RU", ...this.getURLData() }).toString(); try { const urlParams = await this.getHashURLParams(pathname); return await (await this.fetch(`${this.API_ORIGIN + pathname}&${urlParams}`, { method: "POST", headers: this.HEADERS })).json(); } catch (err) { Logger$1.error(`Failed to get weverse InKey by videoId: ${videoId}`, err.message); return false; } } async getVideoInfo(infraVideoId, inkey, serviceId) { const timestamp = Date.now(); try { const urlParams = new URLSearchParams({ key: inkey, sid: serviceId, nonce: timestamp.toString(), devt: "html5_pc", prv: "N", aup: "N", stpb: "N", cpl: "en", env: "prod", lc: "en", adi: JSON.stringify([{ adSystem: null }]), adu: "/" }).toString(); return await (await this.fetch(`https://global.apis.naver.com/rmcnmv/rmcnmv/vod/play/v2.0/${infraVideoId}?` + urlParams, { headers: this.HEADERS })).json(); } catch (err) { Logger$1.error(`Failed to get weverse video info (infraVideoId: ${infraVideoId}, inkey: ${inkey}, serviceId: ${serviceId}`, err.message); return false; } } extractVideoInfo(videoList) { return videoList.find((video) => video.useP2P === false && video.source.includes(".mp4")); } async getVideoData(videoId) { const videoPreview = await this.getPostPreview(videoId); if (!videoPreview) return; const { videoId: internalVideoId, serviceId, infraVideoId } = videoPreview.extension.video; if (!(internalVideoId && serviceId && infraVideoId)) return; const inkeyData = await this.getVideoInKey(internalVideoId); if (!inkeyData) return; const videoInfo = await this.getVideoInfo(infraVideoId, inkeyData.inKey, serviceId); if (!videoInfo) return; const videoItem = this.extractVideoInfo(videoInfo.videos.list); if (!videoItem) return; return { url: videoItem.source, duration: videoItem.duration }; } async getVideoId(url) { return /([^/]+)\/(live|media)\/([^/]+)/.exec(url.pathname)?.[3]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/xhamster.js var XHamsterHelper = class extends BaseHelper { async getVideoId(url) { return /\/(videos\/[^/]+-[\dA-Za-z]+)\/?$/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/xvideos.js var XVideosHelper = class extends BaseHelper { async getVideoId(url) { return /[^/]+\/[^/]+$/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/yandexdisk.js var YandexDiskHelper = class extends BaseHelper { API_ORIGIN = window.location.origin; CLIENT_PREFIX = "/client/disk"; INLINE_PREFIX = "/i/"; DISK_PREFIX = "/d/"; isErrorData(data) { return Object.hasOwn(data, "error"); } async getClientVideoData(videoId) { const dialogId = new URL(window.location.href).searchParams.get("idDialog"); if (!dialogId) return; const preloadedScript = document.querySelector("#preloaded-data"); if (!preloadedScript) return; try { const { idClient, sk } = JSON.parse(preloadedScript.innerText).config; const data = await (await this.fetch(`${this.API_ORIGIN}/models-v2?m=mpfs/info`, { method: "POST", body: JSON.stringify({ apiMethod: "mpfs/info", connection_id: idClient, requestParams: { path: dialogId }, sk }), headers: { "Content-Type": "application/json" } })).json(); if (this.isErrorData(data)) throw new VideoHelperError(data.error?.message ?? data.error?.code); if (data?.type !== "file") throw new VideoHelperError("Failed to get resource info"); const { meta: { short_url, video_info }, name } = data; if (!video_info) throw new VideoHelperError("There's no video open right now"); if (!short_url) throw new VideoHelperError("Access to the video is limited"); const title = this.clearTitle(name); const duration = Math.round(video_info.duration / 1e3); return { url: `https://yadi.sk${videoId}`, video_url: short_url, title, duration, videoId: data.path, translationHelp: [{ target: "video_file_url", targetUrl: short_url }] }; } catch (err) { Logger$1.error(`Failed to get yandex disk video data by video ID: ${videoId}, because ${err.message}`); return; } } clearTitle(title) { return title.replace(/(\.[^.]+)$/, ""); } getBodyHash(fileHash, sk) { const data = JSON.stringify({ hash: fileHash, sk }); return encodeURIComponent(data); } async fetchList(dirHash, sk, offset = 0) { const data = JSON.stringify({ hash: dirHash, sk, offset }); const body = encodeURIComponent(data); const resData = await (await this.fetch(`${this.API_ORIGIN}/public/api/fetch-list`, { method: "POST", body })).json(); if (Object.hasOwn(resData, "error")) throw new VideoHelperError("Failed to fetch folder list"); return resData; } async getDownloadUrl(fileHash, sk) { const body = this.getBodyHash(fileHash, sk); const data = await (await this.fetch(`${this.API_ORIGIN}/public/api/download-url`, { method: "POST", body })).json(); if (data.error) throw new VideoHelperError("Failed to get download url"); return data.data.url; } async getDiskVideoData(videoId) { try { const prefetchEl = document.getElementById("store-prefetch"); if (!prefetchEl) throw new VideoHelperError("Failed to get prefetch data"); const resourcePaths = videoId.split("/").slice(3); if (!resourcePaths.length) throw new VideoHelperError("Failed to find video file path"); const { resources, rootResourceId, environment: { sk } } = JSON.parse(prefetchEl.innerText); const rootResource = resources[rootResourceId]; const fileName = resourcePaths[resourcePaths.length - 1]; const dirPath = resourcePaths.slice(0, -1).join("/"); const expectedPath = dirPath ? `${rootResource.hash}:/${dirPath}/${fileName}` : `${rootResource.hash}:/${fileName}`; let resource = Object.values(resources).find((r) => r.path === expectedPath) || Object.values(resources).find((r) => r.name === fileName && r.type === "file"); if (!resource && dirPath.length > 0) { const folderHash = `${rootResource.hash}:/${dirPath}`; let offset = 0; let completed = false; while (!completed) { const listData = await this.fetchList(folderHash, sk, offset); const fetchedResources = listData.resources || []; if (fetchedResources.length === 0) break; resource = fetchedResources.find((r) => r.name === fileName); if (resource) break; completed = !!listData.completed; offset += fetchedResources.length; } } if (!resource) throw new VideoHelperError("Failed to find resource"); if (resource.type === "dir") throw new VideoHelperError("Path is dir, but expected file"); const { meta: { short_url, mediatype, videoDuration }, path, name } = resource; if (mediatype !== "video") throw new VideoHelperError("Resource isn't a video"); const title = this.clearTitle(name); const duration = videoDuration ? Math.round(videoDuration / 1e3) : 0; if (short_url) return { url: `https://yadi.sk${videoId}`, video_url: short_url, duration, title, videoId: path, translationHelp: [{ target: "video_file_url", targetUrl: short_url }] }; const downloadUrl = await this.getDownloadUrl(path, sk); const proxiedUrl = proxyMedia(new URL(downloadUrl)); return { url: `https://yadi.sk${videoId}`, video_url: downloadUrl, duration, title, videoId: path, translationHelp: [{ target: "video_file_url", targetUrl: proxiedUrl }] }; } catch (err) { Logger$1.error(`Failed to get yandex disk video data by disk video ID: ${videoId}`, err.message); return; } } async getVideoData(videoId) { if (videoId.startsWith(this.INLINE_PREFIX) || /^\/d\/([^/]+)$/.exec(videoId)) return { url: `https://yadi.sk${videoId}` }; videoId = decodeURIComponent(videoId); if (videoId.startsWith(this.CLIENT_PREFIX)) return await this.getClientVideoData(videoId); return await this.getDiskVideoData(videoId); } async getVideoId(url) { if (url.pathname.startsWith(this.CLIENT_PREFIX)) return url.pathname + url.search; const fileId = /\/i\/([^/]+)/.exec(url.pathname)?.[0]; if (fileId) return fileId; return /\/d\/([^/]+)/.exec(url.pathname) ? url.pathname : void 0; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/youku.js var YoukuHelper = class extends BaseHelper { async getVideoId(url) { return /v_show\/id_[\w=]+/.exec(url.pathname)?.[0]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/youtube.js var YoutubeHelper = class YoutubeHelper extends BaseHelper { static isMobile() { return /^m\.youtube\.com$/.test(window.location.hostname); } static extractVideoId(url) { const rawHash = url.hash.replace(/^#/, ""); if (rawHash) { const normalizedHash = rawHash.startsWith("!") ? rawHash.slice(1) : rawHash; let decodedHash = normalizedHash; try { decodedHash = decodeURIComponent(normalizedHash); } catch {} try { const hashUrl = decodedHash.startsWith("http") ? new URL(decodedHash) : new URL(decodedHash.startsWith("/") ? decodedHash : `/${decodedHash}`, url.origin); const hashVideoId = YoutubeHelper.extractVideoId(hashUrl); if (hashVideoId) return hashVideoId; } catch { const hashVideoId = /(?:^|[?&#])v=([^&#]+)/.exec(decodedHash)?.[1]; if (hashVideoId) return hashVideoId; } } if (url.hostname === "youtu.be") return url.pathname.replace(/^\/+/, "").split("/")[0] || void 0; const pathVideoId = /\/(?:watch|embed|shorts|live|v|e)\/([^/?#]+)/.exec(url.pathname)?.[1] ?? void 0; if (pathVideoId) return pathVideoId; return url.searchParams.get("v") ?? void 0; } static getPlayer() { if (window.location.pathname.startsWith("/shorts/") && !YoutubeHelper.isMobile()) return document.querySelector("#shorts-player"); return document.querySelector("#movie_player"); } static getPlayerResponse() { return YoutubeHelper.getPlayer()?.getPlayerResponse?.call(void 0); } static getPlayerData() { return YoutubeHelper.getPlayer()?.getVideoData?.call(void 0); } /** * @returns volume in 0.00 - 1.00 range */ static getVolume() { const player = YoutubeHelper.getPlayer(); if (player?.getVolume) return player.getVolume() / 100; return 1; } static setVolume(volume) { const player = YoutubeHelper.getPlayer(); if (player?.setVolume) { player.setVolume(Math.round(volume * 100)); return true; } return false; } static isMuted() { const player = YoutubeHelper.getPlayer(); if (player?.isMuted) return player.isMuted(); return false; } /** * time in ms */ static videoSeek(video, time) { Logger$1.log("videoSeek", time); video.currentTime = (YoutubeHelper.getPlayer()?.getProgressState()?.seekableEnd ?? video.currentTime) - time; } /** * i guess it doesn't work on mobile. Maybe I'll fix it later */ static getPoToken() { const player = YoutubeHelper.getPlayer(); if (!player) return; const audioTrack = player.getAudioTrack?.call(void 0); if (!audioTrack?.captionTracks?.length) return; const audioTrackWithPoToken = audioTrack.captionTracks.find((captionTrack) => captionTrack.url.includes("&pot=")); if (!audioTrackWithPoToken) return; return /&pot=([^&]+)/.exec(audioTrackWithPoToken.url)?.[1]; } static getGlobalConfig() { if (typeof yt !== "undefined") return yt?.config_; return typeof ytcfg !== "undefined" ? ytcfg?.data_ : void 0; } static getDeviceParams() { const ytconfig = YoutubeHelper.getGlobalConfig(); if (!ytconfig) return "c=WEB"; const innertubeClient = ytconfig.INNERTUBE_CONTEXT?.client; const deviceParams = new URLSearchParams(ytconfig.DEVICE); deviceParams.delete("ceng"); deviceParams.delete("cengver"); deviceParams.set("c", innertubeClient?.clientName ?? ytconfig.INNERTUBE_CLIENT_NAME); deviceParams.set("cver", innertubeClient?.clientVersion ?? ytconfig.INNERTUBE_CLIENT_VERSION); deviceParams.set("cplayer", "UNIPLAYER"); return deviceParams.toString(); } static getSubtitles(userLang) { const playerCaptions = YoutubeHelper.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer; if (!playerCaptions) return []; const captionTracks = playerCaptions.captionTracks ?? []; const userLangSupported = (playerCaptions.translationLanguages ?? []).find((language) => language.languageCode === userLang); const asrLang = captionTracks.find((captionTrack) => captionTrack?.kind === "asr")?.languageCode ?? "en"; const subtitles = captionTracks.reduce((result, captionTrack) => { if (!("languageCode" in captionTrack)) return result; const language = captionTrack.languageCode ? normalizeLang$1(captionTrack.languageCode) : void 0; const url = captionTrack.baseUrl; if (!language || !url) return result; const captionUrl = `${url.startsWith("http") ? url : `${window.location.origin}/${url}`}&fmt=json3`; result.push({ source: "youtube", format: "json", language, isAutoGenerated: captionTrack?.kind === "asr", url: captionUrl }); if (userLangSupported && captionTrack.isTranslatable && captionTrack.languageCode === asrLang && userLang !== language) result.push({ source: "youtube", format: "json", language: userLang, isAutoGenerated: captionTrack?.kind === "asr", translatedFromLanguage: language, url: `${captionUrl}&tlang=${userLang}` }); return result; }, []); Logger$1.log("youtube subtitles:", subtitles); return subtitles; } static getLanguage() { if (!YoutubeHelper.isMobile()) { const trackInfo = YoutubeHelper.getPlayer()?.getAudioTrack?.call(void 0)?.getLanguageInfo(); if (trackInfo && trackInfo.id !== "und") return normalizeLang$1(trackInfo.id.split(".")[0]); } const autoCaption = YoutubeHelper.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer.captionTracks.find((caption) => caption.kind === "asr" && caption.languageCode); return autoCaption ? normalizeLang$1(autoCaption.languageCode) : void 0; } async getVideoData(videoId) { const { title: localizedTitle } = YoutubeHelper.getPlayerData() ?? {}; const { shortDescription: description, isLive: isStream, title } = YoutubeHelper.getPlayerResponse()?.videoDetails ?? {}; const subtitles = YoutubeHelper.getSubtitles(this.language); let detectedLanguage = YoutubeHelper.getLanguage(); if (detectedLanguage && !availableLangs.includes(detectedLanguage)) detectedLanguage = void 0; const duration = YoutubeHelper.getPlayer()?.getDuration?.call(void 0) ?? void 0; return { url: this.service?.url + videoId, isStream, title, localizedTitle, detectedLanguage, description, subtitles, duration }; } async getVideoId(url) { if (url.searchParams.has("enablejsapi")) { const videoUrl = YoutubeHelper.getPlayer()?.getVideoUrl(); url = videoUrl ? new URL(videoUrl) : url; } return YoutubeHelper.extractVideoId(url); } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/zdf.js var zdfPlayPathRe = /^\/play\/([^/?#]+)\/([^/?#]+)\/([^/?#]+)\/?$/i; var ZDFHelper = class extends BaseHelper { async getVideoId(url) { const match = zdfPlayPathRe.exec(url.pathname); if (!match) return; const [, publicationForm, collectionCanonical, videoCanonical] = match; return `${publicationForm}/${collectionCanonical}/${videoCanonical}`; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/wistia.js var WistiaHelper = class extends BaseHelper { async getVideoId(url) { return /\/embed\/iframe\/([^/]+)/.exec(url.pathname)?.[1]; } }; //#endregion //#region node_modules/@vot.js/ext/dist/helpers/index.js var availableHelpers = { [VideoService$1.mailru]: MailRuHelper, [VideoService$1.weverse]: WeverseHelper, [VideoService$1.weibo]: WeiboHelper, [VideoService$1.kodik]: KodikHelper, [VideoService$1.patreon]: PatreonHelper, [VideoService$1.reddit]: RedditHelper, [VideoService$1.bannedvideo]: BannedVideoHelper, [VideoService$1.kick]: KickHelper, [VideoService$1.appledeveloper]: AppleDeveloperHelper, [VideoService$1.epicgames]: EpicGamesHelper, [VideoService$1.odysee]: OdyseeHelper, [VideoService$1.coursehunterLike]: CoursehunterLikeHelper, [VideoService$1.twitch]: TwitchHelper, [VideoService$1.sap]: SapHelper, [VideoService$1.jove]: JoveHelper, [VideoService$1.linkedin]: LinkedinHelper, [VideoService$1.vimeo]: VimeoHelper, [VideoService$1.yandexdisk]: YandexDiskHelper, [VideoService$1.vk]: VKHelper, [VideoService$1.trovo]: TrovoHelper, [VideoService$1.porntn]: PornTNHelper, [VideoService$1.googledrive]: GoogleDriveHelper, [VideoService$1.bilibili]: BilibiliHelper, [VideoService$1.xvideos]: XVideosHelper, [VideoService$1.xhamster]: XHamsterHelper, [VideoService$1.spankbang]: SpankBangHelper, [VideoService$1.rule34video]: Rule34VideoHelper, [VideoService$1.picarto]: PicartoHelper, [VideoService$1.olympicsreplay]: OlympicsReplayHelper, [VideoService$1.watchpornto]: WatchPornToHelper, [VideoService$1.archive]: ArchiveHelper, [VideoService$1.dailymotion]: DailymotionHelper, [VideoService$1.youku]: YoukuHelper, [VideoService$1.egghead]: EggheadHelper, [VideoService$1.newgrounds]: NewgroundsHelper, [VideoService$1.okru]: OKRuHelper, [VideoService$1.peertube]: PeertubeHelper, [VideoService$1.eporner]: EpornerHelper, [VideoService$1.bitchute]: BitchuteHelper, [VideoService$1.rutube]: RutubeHelper, [VideoService$1.facebook]: FacebookHelper, [VideoService$1.rumble]: RumbleHelper, [VideoService$1.twitter]: TwitterHelper, [VideoService$1.pornhub]: PornhubHelper, [VideoService$1.tiktok]: TikTokHelper, [VideoService$1.proxitok]: TikTokHelper, [VideoService$1.nine_gag]: NineGAGHelper, [VideoService$1.youtube]: YoutubeHelper, [VideoService$1.preservetube]: PreserveTubeHelper, [VideoService$1.invidious]: YoutubeHelper, [VideoService$1.piped]: YoutubeHelper, [VideoService$1.zdf]: ZDFHelper, [VideoService$1.dzen]: DzenHelper, [VideoService$1.bunnystream]: BunnyStreamHelper, [VideoService$1.cloudflarestream]: CloudflareStreamHelper, [VideoService$1.loom]: LoomHelper, [VideoService$1.rtnews]: RtNewsHelper, [VideoService$1.bitview]: BitviewHelper, [VideoService$1.thisvid]: ThisVidHelper, [VideoService$1.joidatabase]: JOIDatabaseHelper, [VideoService$1.ign]: IgnHelper, [VideoService$1.bunkr]: BunkrHelper, [VideoService$1.imdb]: IMDbHelper, [VideoService$1.telegram]: TelegramHelper, [VideoService$1.niconico]: NicoNicoHelper, [VideoService$1.noodlemagazine]: NoodleMagazineHelper, [VideoService$1.wistia]: WistiaHelper, [ExtVideoService.udemy]: UdemyHelper, [ExtVideoService.coursera]: CourseraHelper, [ExtVideoService.douyin]: DouyinHelper, [ExtVideoService.artstation]: ArtstationHelper, [ExtVideoService.kickstarter]: KickstarterHelper, [ExtVideoService.datacamp]: DataCampHelper, [ExtVideoService.oraclelearn]: OracleLearnHelper, [ExtVideoService.deeplearningai]: DeeplearningAIHelper, [ExtVideoService.netacad]: NetacadHelper, [ExtVideoService.mediafile]: MediafileHelper, [ExtVideoService.skilljar]: SkilljarHelper, [ExtVideoService.dropout]: DropoutHelper }; /** * A convenient wrapper over the rest of the helpers */ var VideoHelper = class { helpersData; constructor(helpersData = {}) { this.helpersData = helpersData; } getHelper(service) { return new availableHelpers[service](this.helpersData); } }; //#endregion //#region node_modules/@vot.js/ext/dist/utils/videoData.js function hasHelper(host) { return host in availableHelpers; } function getService() { if (localLinkRe.exec(window.location.href)) return []; const hostname = window.location.hostname; const enteredURL = new URL(window.location.href); const isMatches = (match) => { if (match instanceof RegExp) return match.test(hostname); else if (typeof match === "string") return hostname.includes(match); else if (typeof match === "function") return match(enteredURL); return false; }; return sites_default.filter((e) => { return !!e.match && (Array.isArray(e.match) ? e.match.some(isMatches) : isMatches(e.match)) && e.host && e.url; }); } async function getVideoID(service, opts = {}) { const url = new URL(window.location.href); const serviceHost = service.host; if (hasHelper(serviceHost)) return await new VideoHelper(opts).getHelper(serviceHost).getVideoId(url); return serviceHost === VideoService$1.custom ? url.href : void 0; } async function getVideoData(service, opts = {}) { const currentUrl = new URL(window.location.href); const videoId = await getVideoID(service, opts); if (!videoId) throw new VideoDataError(`Entered unsupported link: "${service.host}"`); const origin = currentUrl.origin; if ([ VideoService$1.peertube, VideoService$1.coursehunterLike, VideoService$1.bunnystream, VideoService$1.cloudflarestream ].includes(service.host)) service.url = origin; if (service.rawResult) return { url: videoId, videoId, host: service.host, duration: void 0 }; if (!service.needExtraData) return { url: service.url + videoId, videoId, host: service.host, duration: void 0 }; if (!hasHelper(service.host)) throw new VideoDataError(`No helper is available for "${service.host}"`); const result = await new VideoHelper({ ...opts, service, origin }).getHelper(service.host).getVideoData(videoId); if (!result) throw new VideoDataError(`Failed to get video raw url for ${service.host}`); return { ...result, url: result.url, videoId, host: service.host }; } //#endregion //#region src/bootstrap/bootState.ts var MAIN_BOOT_KEY = "__VOT_MAIN_BOOT_STATE__"; var BOOTSTRAP_STATUSES = /* @__PURE__ */ new Set([ "idle", "booting", "booted", "failed" ]); function isBootstrapStatus(value) { return BOOTSTRAP_STATUSES.has(value); } function isBootstrapState(value) { if (!value || typeof value !== "object") return false; return isBootstrapStatus(value.status); } function getOrCreateBootState(bootKey = MAIN_BOOT_KEY) { const scope = globalThis; const existing = scope[bootKey]; if (isBootstrapState(existing)) return existing; const created = { status: "idle", error: null }; scope[bootKey] = created; return created; } //#endregion //#region src/bootstrap/iframeInteractor.ts var iframeInteractorInitialized = false; var IFRAME_CONFIGS = { "https://dev.epicgames.com": { targetOrigin: "https://dev.epicgames.com", dataFilter: (data) => typeof data === "string" && data.startsWith("getVideoId:"), extractVideoId: (url) => url.pathname.split("/").at(-2) ?? null, responseFormatter: (videoId, data) => `${typeof data === "string" ? data : ""}:${videoId}` }, "https://www.dailymotion.com": { targetOrigin: "https://geo.dailymotion.com", dataFilter: (data) => typeof data === "string" && data.startsWith("getVideoId:"), extractVideoId: (url) => /(?:^|\/)video\/([^/]+)/.exec(url.pathname)?.[1] ?? null, responseFormatter: (videoId) => `getVideoId:${videoId}` } }; function initIframeInteractor() { if (iframeInteractorInitialized) return; iframeInteractorInitialized = true; const currentConfig = IFRAME_CONFIGS[globalThis.location.origin]; if (!currentConfig) return; globalThis.addEventListener("message", (event) => { try { if (event.origin !== currentConfig.targetOrigin) return; if (!currentConfig.dataFilter(event.data)) return; const videoId = currentConfig.extractVideoId(new URL(globalThis.location.href)); if (!videoId) return; const response = currentConfig.responseFormatter(videoId, event.data); event.source?.postMessage(response, currentConfig.targetOrigin); } catch (error) { console.error("Iframe communication error:", error); } }); } //#endregion //#region src/config/config.ts var workerHost = "api.browser.yandex.ru"; /** * used for streaming * * @see https://github.com/FOSWLY/media-proxy */ var m3u8ProxyHost = "media-proxy.toil.cc/v1/proxy/m3u8"; /** * @see https://github.com/FOSWLY/vot-worker */ var proxyWorkerHostMode1 = "vot-worker.vtrans.eu.cc"; var proxyWorkerHost = "vot-worker.eu.cc"; /** * @see https://github.com/FOSWLY/translate-backend */ var foswlyTranslateUrl = "https://translate-backend.transly.eu.cc/v2"; var detectRustServerUrl = "https://rust-server-531j.onrender.com/detect"; var authServerUrl = "https://rust-server-531j.onrender.com"; var authLoginUrl = `${authServerUrl}/v1/auth/handle`; var avatarServerUrl = "https://avatars.mds.yandex.net/get-yapic"; var repoPath = "ilyhalight/voice-over-translation"; var contentUrl = `https://raw.githubusercontent.com/${repoPath}`; var repositoryUrl = `https://github.com/${repoPath}`; var defaultTranslationService = "yandexbrowser"; var defaultDetectService = "yandexbrowser"; var proxyOnlyCountries = [ "UA", "LV", "LT" ]; /** * 100 - 3000 ms - delay before hiding button */ var defaultAutoHideDelay = 1e3; var actualCompatVersion = "2025-05-09"; //#endregion //#region src/types/storage.ts var subtitleResponseLanguageModes = ["auto", "original"]; var storageKeys = [ "autoTranslate", "autoSubtitles", "dontTranslateLanguages", "enabledDontTranslateLanguages", "enabledAutoVolume", "enabledSmartDucking", "autoVolume", "buttonPos", "showVideoSlider", "syncVolume", "downloadWithName", "sendNotifyOnComplete", "subtitlesMaxLength", "subtitlesSmartLayout", "highlightWords", "subtitlesFontSize", "subtitlesFontFamily", "subtitlesOpacity", "subtitlesDownloadFormat", "responseLanguage", "responseLanguageSubtitles", "defaultVolume", "onlyBypassMediaCSP", "newAudioPlayer", "showPiPButton", "translateAPIErrors", "translationService", "detectService", "translationHotkey", "subtitlesHotkey", "m3u8ProxyHost", "proxyWorkerHost", "translateProxyEnabled", "translateProxyEnabledDefault", "audioBooster", "useLivelyVoice", "autoHideButtonDelay", "useAudioDownload", "compatVersion", "localePhrases", "localeLang", "localeHash", "localeVersion", "localeUpdatedAt", "localeLangOverride", "account" ]; //#endregion //#region src/utils/debug.ts var log = (...text) => { console.log("%c[VOT DEBUG]", "background: #3700ffff; color: #fff; padding: 5px;", ...text); }; var warn = (...text) => { console.warn("%c[VOT DEBUG]", "background: #e1ff00ff; color: #fff; padding: 5px;", ...text); }; var error = (...text) => { console.error("%c[VOT DEBUG]", "background: #F2452D; color: #fff; padding: 5px;", ...text); }; var debug = { log, warn, error }; //#endregion //#region src/utils/errors.ts /** * Small error helpers used across the project. */ function stringifyUnknownObject(value) { const seen = /* @__PURE__ */ new WeakSet(); try { return JSON.stringify(value, (_key, currentValue) => { if (typeof currentValue !== "object" || currentValue === null) return currentValue; if (seen.has(currentValue)) return "[Circular]"; seen.add(currentValue); return currentValue; }) ?? null; } catch { return null; } } function extractNestedMessage(error) { const candidates = [ error?.data?.message, error?.error?.message, error?.message ]; for (const candidate of candidates) if (typeof candidate === "string" && candidate) return candidate; return null; } function formatObjectError(error, fallback) { const nestedMessage = extractNestedMessage(error); if (nestedMessage) return nestedMessage; const serialized = stringifyUnknownObject(error); if (serialized && serialized !== "{}") return serialized; const ctorName = error.constructor?.name; return ctorName ? `[${ctorName}]` : fallback; } function formatPrimitiveError(error, fallback) { if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return `${error}`; if (typeof error === "symbol") return error.description ? `Symbol(${error.description})` : "Symbol"; if (typeof error === "function") return error.name ? `[Function ${error.name}]` : "[Function]"; return fallback; } function toErrorMessage(error, fallback = "Unknown error") { if (error instanceof Error) return error.message || fallback; if (typeof error === "string") return error || fallback; if (error === null || error === void 0) return fallback; if (typeof error === "object") return formatObjectError(error, fallback); return formatPrimitiveError(error, fallback); } /** * Extracts a human-readable error message from various error shapes. */ function getErrorMessage(error) { return toErrorMessage(error, ""); } function isAbortError(err) { const anyErr = err; return typeof DOMException !== "undefined" && anyErr instanceof DOMException && anyErr.name === "AbortError" || anyErr instanceof Error && anyErr.name === "AbortError" || anyErr?.message === "AbortError"; } /** * Creates a canonical AbortError instance. Prefer DOMException when available. * * Note: This is intentionally not coupled to AbortSignal.reason to avoid * surfacing string/opaque abort reasons as user-facing "errors". */ function makeAbortError(reason = "Aborted") { if (reason instanceof Error && isAbortError(reason)) return reason; const message = reason instanceof Error ? reason.message : String(reason ?? "Aborted"); try { return new DOMException(message, "AbortError"); } catch { const err = new Error(message); err.name = "AbortError"; return err; } } /** * Safely reads a nested field from an unknown value. * * Returns `undefined` when any intermediate property is missing or not an * object, eliminating the repetitive null-check → cast → access pattern * that was scattered across error-handling code. * * @example * safeNestedGet(err, ["data", "message"]) // err?.data?.message * safeNestedGet(err, ["status"]) // err?.status */ function safeNestedGet(value, path) { let current = value; for (const key of path) { if (current == null || typeof current !== "object") return; current = current[key]; } return current; } //#endregion //#region src/utils/abort.ts var NEVER_ABORTED_SIGNAL = new AbortController().signal; /** * Throws a canonical AbortError if the provided signal is aborted. * * Runtimes that implement `AbortSignal.throwIfAborted()` throw `signal.reason`, * which can be *any* value. We normalize cancellation to a standard * `AbortError` so callers can reliably use `isAbortError()`. */ function throwIfAborted(signal) { try { signal.throwIfAborted(); } catch (e) { if (signal.aborted || isAbortError(e)) throw makeAbortError(); throw e instanceof Error ? e : new Error(String(e)); } } /** * Creates an AbortSignal that auto-aborts after `timeoutMs`. * * If an `external` signal is provided, the returned signal is aborted when * *either* external aborts or the timeout elapses. */ function createTimeoutSignal(timeoutMs, external) { if (!(Number.isFinite(timeoutMs) && timeoutMs > 0)) return { signal: external ?? NEVER_ABORTED_SIGNAL, cleanup: () => {} }; const controller = new AbortController(); let timeoutId; let cleaned = false; const cleanup = () => { if (cleaned) return; cleaned = true; if (timeoutId !== void 0) { clearTimeout(timeoutId); timeoutId = void 0; } external?.removeEventListener("abort", onExternalAbort); }; const onExternalAbort = () => { cleanup(); controller.abort(external?.reason); }; if (external) { external.addEventListener("abort", onExternalAbort, { once: true }); if (external.aborted) { onExternalAbort(); return { signal: controller.signal, cleanup }; } } timeoutId = setTimeout(() => { cleanup(); controller.abort(makeAbortError("Timeout")); }, timeoutMs); return { signal: controller.signal, cleanup }; } /** * Returns a promise that resolves after `delayMs` and rejects if `signal` * is aborted before the delay elapses. * * Unlike `createAbortableWaiter`, the timeout here is a *delay* — the promise * resolves on expiry so the caller can proceed with the next action (e.g. * a retry). The promise only rejects when the external `signal` is aborted * (i.e. the operation was cancelled). */ function createAbortableDelay(delayMs, signal, options) { return new Promise((resolve, reject) => { if (signal.aborted) { reject(makeAbortError()); return; } let settled = false; const timeoutId = setTimeout(() => { if (settled) return; settled = true; signal.removeEventListener("abort", onAbort); resolve(); }, delayMs); options?.onScheduled?.(timeoutId); function onAbort() { if (settled) return; settled = true; clearTimeout(timeoutId); reject(makeAbortError()); } signal.addEventListener("abort", onAbort, { once: true }); }); } /** * Generic abortable waiter that unifies the "create a Promise that settles * on timeout, abort-signal, or external event" pattern. * * Previously duplicated between `waitForAbortableTimeout` (timeout-driven) * and `waitForAudioDownloadCompletion` (event-driven with external settle). * * Uses `AbortController` + `AbortSignal.any()` for modern runtimes, * falling back to manual `addEventListener` for older environments. * * @returns The promise and a `settle` handle for external resolve/reject. */ function createAbortableWaiter(signal, timeoutMs, options) { let settled = false; let _resolve; let _reject; const promise = new Promise((res, rej) => { _resolve = res; _reject = rej; }); let timeoutId; const cleanup = () => { if (settled) return; settled = true; if (timeoutId !== void 0) { clearTimeout(timeoutId); timeoutId = void 0; } options?.onSettled?.(); }; if (typeof AbortSignal.any === "function") { const timeoutSignal = AbortSignal.timeout(timeoutMs); options?.onScheduled?.(void 0); const combined = AbortSignal.any([signal, timeoutSignal].filter(Boolean)); const onAbort = () => { cleanup(); _reject(makeAbortError(timeoutSignal.aborted ? "Timeout" : "Aborted")); }; if (combined.aborted) onAbort(); else combined.addEventListener("abort", onAbort, { once: true }); } else { const onAbort = () => { cleanup(); _reject(makeAbortError()); }; signal.addEventListener("abort", onAbort, { once: true }); if (signal.aborted) onAbort(); else { timeoutId = setTimeout(() => { cleanup(); _reject(makeAbortError("Timeout")); }, timeoutMs); options?.onScheduled?.(timeoutId); } } return { promise, settle: { resolve: () => { cleanup(); _resolve(); }, reject: (error) => { cleanup(); _reject(error); } } }; } var browserInfo = (/* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => { (function(e, t) { "object" == typeof exports && "object" == typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.bowser = t() : e.bowser = t(); })(exports, (function() { return function(e) { var t = {}; function r(i) { if (t[i]) return t[i].exports; var n = t[i] = { i, l: !1, exports: {} }; return e[i].call(n.exports, n, n.exports, r), n.l = !0, n.exports; } return r.m = e, r.c = t, r.d = function(e, t, i) { r.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: i }); }, r.r = function(e) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }); }, r.t = function(e, t) { if (1 & t && (e = r(e)), 8 & t) return e; if (4 & t && "object" == typeof e && e && e.__esModule) return e; var i = Object.create(null); if (r.r(i), Object.defineProperty(i, "default", { enumerable: !0, value: e }), 2 & t && "string" != typeof e) for (var n in e) r.d(i, n, function(t) { return e[t]; }.bind(null, n)); return i; }, r.n = function(e) { var t = e && e.__esModule ? function() { return e.default; } : function() { return e; }; return r.d(t, "a", t), t; }, r.o = function(e, t) { return Object.prototype.hasOwnProperty.call(e, t); }, r.p = "", r(r.s = 90); }({ 17: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i = r(18); t.default = function() { function e() {} return e.getFirstMatch = function(e, t) { var r = t.match(e); return r && r.length > 0 && r[1] || ""; }, e.getSecondMatch = function(e, t) { var r = t.match(e); return r && r.length > 1 && r[2] || ""; }, e.matchAndReturnConst = function(e, t, r) { if (e.test(t)) return r; }, e.getWindowsVersionName = function(e) { switch (e) { case "NT": return "NT"; case "XP": return "XP"; case "NT 5.0": return "2000"; case "NT 5.1": return "XP"; case "NT 5.2": return "2003"; case "NT 6.0": return "Vista"; case "NT 6.1": return "7"; case "NT 6.2": return "8"; case "NT 6.3": return "8.1"; case "NT 10.0": return "10"; default: return; } }, e.getMacOSVersionName = function(e) { var t = e.split(".").splice(0, 2).map((function(e) { return parseInt(e, 10) || 0; })); t.push(0); var r = t[0], i = t[1]; if (10 === r) switch (i) { case 5: return "Leopard"; case 6: return "Snow Leopard"; case 7: return "Lion"; case 8: return "Mountain Lion"; case 9: return "Mavericks"; case 10: return "Yosemite"; case 11: return "El Capitan"; case 12: return "Sierra"; case 13: return "High Sierra"; case 14: return "Mojave"; case 15: return "Catalina"; default: return; } switch (r) { case 11: return "Big Sur"; case 12: return "Monterey"; case 13: return "Ventura"; case 14: return "Sonoma"; case 15: return "Sequoia"; default: return; } }, e.getAndroidVersionName = function(e) { var t = e.split(".").splice(0, 2).map((function(e) { return parseInt(e, 10) || 0; })); if (t.push(0), !(1 === t[0] && t[1] < 5)) return 1 === t[0] && t[1] < 6 ? "Cupcake" : 1 === t[0] && t[1] >= 6 ? "Donut" : 2 === t[0] && t[1] < 2 ? "Eclair" : 2 === t[0] && 2 === t[1] ? "Froyo" : 2 === t[0] && t[1] > 2 ? "Gingerbread" : 3 === t[0] ? "Honeycomb" : 4 === t[0] && t[1] < 1 ? "Ice Cream Sandwich" : 4 === t[0] && t[1] < 4 ? "Jelly Bean" : 4 === t[0] && t[1] >= 4 ? "KitKat" : 5 === t[0] ? "Lollipop" : 6 === t[0] ? "Marshmallow" : 7 === t[0] ? "Nougat" : 8 === t[0] ? "Oreo" : 9 === t[0] ? "Pie" : void 0; }, e.getVersionPrecision = function(e) { return e.split(".").length; }, e.compareVersions = function(t, r, i) { void 0 === i && (i = !1); var n = e.getVersionPrecision(t), a = e.getVersionPrecision(r), o = Math.max(n, a), s = 0, u = e.map([t, r], (function(t) { var r = o - e.getVersionPrecision(t), i = t + new Array(r + 1).join(".0"); return e.map(i.split("."), (function(e) { return new Array(20 - e.length).join("0") + e; })).reverse(); })); for (i && (s = o - Math.min(n, a)), o -= 1; o >= s;) { if (u[0][o] > u[1][o]) return 1; if (u[0][o] === u[1][o]) { if (o === s) return 0; o -= 1; } else if (u[0][o] < u[1][o]) return -1; } }, e.map = function(e, t) { var r, i = []; if (Array.prototype.map) return Array.prototype.map.call(e, t); for (r = 0; r < e.length; r += 1) i.push(t(e[r])); return i; }, e.find = function(e, t) { var r, i; if (Array.prototype.find) return Array.prototype.find.call(e, t); for (r = 0, i = e.length; r < i; r += 1) { var n = e[r]; if (t(n, r)) return n; } }, e.assign = function(e) { for (var t, r, i = e, n = arguments.length, a = new Array(n > 1 ? n - 1 : 0), o = 1; o < n; o++) a[o - 1] = arguments[o]; if (Object.assign) return Object.assign.apply(Object, [e].concat(a)); var s = function() { var e = a[t]; "object" == typeof e && null !== e && Object.keys(e).forEach((function(t) { i[t] = e[t]; })); }; for (t = 0, r = a.length; t < r; t += 1) s(); return e; }, e.getBrowserAlias = function(e) { return i.BROWSER_ALIASES_MAP[e]; }, e.getBrowserTypeByAlias = function(e) { return i.BROWSER_MAP[e] || ""; }, e; }(), e.exports = t.default; }, 18: function(e, t, r) { "use strict"; t.__esModule = !0, t.ENGINE_MAP = t.OS_MAP = t.PLATFORMS_MAP = t.BROWSER_MAP = t.BROWSER_ALIASES_MAP = void 0; t.BROWSER_ALIASES_MAP = { AmazonBot: "amazonbot", "Amazon Silk": "amazon_silk", "Android Browser": "android", BaiduSpider: "baiduspider", Bada: "bada", BingCrawler: "bingcrawler", Brave: "brave", BlackBerry: "blackberry", "ChatGPT-User": "chatgpt_user", Chrome: "chrome", ClaudeBot: "claudebot", Chromium: "chromium", Diffbot: "diffbot", DuckDuckBot: "duckduckbot", DuckDuckGo: "duckduckgo", Electron: "electron", Epiphany: "epiphany", FacebookExternalHit: "facebookexternalhit", Firefox: "firefox", Focus: "focus", Generic: "generic", "Google Search": "google_search", Googlebot: "googlebot", GPTBot: "gptbot", "Internet Explorer": "ie", InternetArchiveCrawler: "internetarchivecrawler", "K-Meleon": "k_meleon", LibreWolf: "librewolf", Linespider: "linespider", Maxthon: "maxthon", "Meta-ExternalAds": "meta_externalads", "Meta-ExternalAgent": "meta_externalagent", "Meta-ExternalFetcher": "meta_externalfetcher", "Meta-WebIndexer": "meta_webindexer", "Microsoft Edge": "edge", "MZ Browser": "mz", "NAVER Whale Browser": "naver", "OAI-SearchBot": "oai_searchbot", Omgilibot: "omgilibot", Opera: "opera", "Opera Coast": "opera_coast", "Pale Moon": "pale_moon", PerplexityBot: "perplexitybot", "Perplexity-User": "perplexity_user", PhantomJS: "phantomjs", PingdomBot: "pingdombot", Puffin: "puffin", QQ: "qq", QQLite: "qqlite", QupZilla: "qupzilla", Roku: "roku", Safari: "safari", Sailfish: "sailfish", "Samsung Internet for Android": "samsung_internet", SlackBot: "slackbot", SeaMonkey: "seamonkey", Sleipnir: "sleipnir", "Sogou Browser": "sogou", Swing: "swing", Tizen: "tizen", "UC Browser": "uc", Vivaldi: "vivaldi", "WebOS Browser": "webos", WeChat: "wechat", YahooSlurp: "yahooslurp", "Yandex Browser": "yandex", YandexBot: "yandexbot", YouBot: "youbot" }; t.BROWSER_MAP = { amazonbot: "AmazonBot", amazon_silk: "Amazon Silk", android: "Android Browser", baiduspider: "BaiduSpider", bada: "Bada", bingcrawler: "BingCrawler", blackberry: "BlackBerry", brave: "Brave", chatgpt_user: "ChatGPT-User", chrome: "Chrome", claudebot: "ClaudeBot", chromium: "Chromium", diffbot: "Diffbot", duckduckbot: "DuckDuckBot", duckduckgo: "DuckDuckGo", edge: "Microsoft Edge", electron: "Electron", epiphany: "Epiphany", facebookexternalhit: "FacebookExternalHit", firefox: "Firefox", focus: "Focus", generic: "Generic", google_search: "Google Search", googlebot: "Googlebot", gptbot: "GPTBot", ie: "Internet Explorer", internetarchivecrawler: "InternetArchiveCrawler", k_meleon: "K-Meleon", librewolf: "LibreWolf", linespider: "Linespider", maxthon: "Maxthon", meta_externalads: "Meta-ExternalAds", meta_externalagent: "Meta-ExternalAgent", meta_externalfetcher: "Meta-ExternalFetcher", meta_webindexer: "Meta-WebIndexer", mz: "MZ Browser", naver: "NAVER Whale Browser", oai_searchbot: "OAI-SearchBot", omgilibot: "Omgilibot", opera: "Opera", opera_coast: "Opera Coast", pale_moon: "Pale Moon", perplexitybot: "PerplexityBot", perplexity_user: "Perplexity-User", phantomjs: "PhantomJS", pingdombot: "PingdomBot", puffin: "Puffin", qq: "QQ Browser", qqlite: "QQ Browser Lite", qupzilla: "QupZilla", roku: "Roku", safari: "Safari", sailfish: "Sailfish", samsung_internet: "Samsung Internet for Android", seamonkey: "SeaMonkey", slackbot: "SlackBot", sleipnir: "Sleipnir", sogou: "Sogou Browser", swing: "Swing", tizen: "Tizen", uc: "UC Browser", vivaldi: "Vivaldi", webos: "WebOS Browser", wechat: "WeChat", yahooslurp: "YahooSlurp", yandex: "Yandex Browser", yandexbot: "YandexBot", youbot: "YouBot" }; t.PLATFORMS_MAP = { bot: "bot", desktop: "desktop", mobile: "mobile", tablet: "tablet", tv: "tv" }; t.OS_MAP = { Android: "Android", Bada: "Bada", BlackBerry: "BlackBerry", ChromeOS: "Chrome OS", HarmonyOS: "HarmonyOS", iOS: "iOS", Linux: "Linux", MacOS: "macOS", PlayStation4: "PlayStation 4", Roku: "Roku", Tizen: "Tizen", WebOS: "WebOS", Windows: "Windows", WindowsPhone: "Windows Phone" }; t.ENGINE_MAP = { Blink: "Blink", EdgeHTML: "EdgeHTML", Gecko: "Gecko", Presto: "Presto", Trident: "Trident", WebKit: "WebKit" }; }, 90: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i, n = (i = r(91)) && i.__esModule ? i : { default: i }, a = r(18); function o(e, t) { for (var r = 0; r < t.length; r++) { var i = t[r]; i.enumerable = i.enumerable || !1, i.configurable = !0, "value" in i && (i.writable = !0), Object.defineProperty(e, i.key, i); } } t.default = function() { function e() {} var t, r, i; return e.getParser = function(e, t, r) { if (void 0 === t && (t = !1), void 0 === r && (r = null), "string" != typeof e) throw new Error("UserAgent should be a string"); return new n.default(e, t, r); }, e.parse = function(e, t) { return void 0 === t && (t = null), new n.default(e, t).getResult(); }, t = e, i = [ { key: "BROWSER_MAP", get: function() { return a.BROWSER_MAP; } }, { key: "ENGINE_MAP", get: function() { return a.ENGINE_MAP; } }, { key: "OS_MAP", get: function() { return a.OS_MAP; } }, { key: "PLATFORMS_MAP", get: function() { return a.PLATFORMS_MAP; } } ], (r = null) && o(t.prototype, r), i && o(t, i), e; }(), e.exports = t.default; }, 91: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i = u(r(92)), n = u(r(93)), a = u(r(94)), o = u(r(95)), s = u(r(17)); function u(e) { return e && e.__esModule ? e : { default: e }; } t.default = function() { function e(e, t, r) { if (void 0 === t && (t = !1), void 0 === r && (r = null), null == e || "" === e) throw new Error("UserAgent parameter can't be empty"); this._ua = e; var i = !1; "boolean" == typeof t ? (i = t, this._hints = r) : this._hints = null != t && "object" == typeof t ? t : null, this.parsedResult = {}, !0 !== i && this.parse(); } var t = e.prototype; return t.getHints = function() { return this._hints; }, t.hasBrand = function(e) { if (!this._hints || !Array.isArray(this._hints.brands)) return !1; var t = e.toLowerCase(); return this._hints.brands.some((function(e) { return e.brand && e.brand.toLowerCase() === t; })); }, t.getBrandVersion = function(e) { if (this._hints && Array.isArray(this._hints.brands)) { var t = e.toLowerCase(), r = this._hints.brands.find((function(e) { return e.brand && e.brand.toLowerCase() === t; })); return r ? r.version : void 0; } }, t.getUA = function() { return this._ua; }, t.test = function(e) { return e.test(this._ua); }, t.parseBrowser = function() { var e = this; this.parsedResult.browser = {}; var t = s.default.find(i.default, (function(t) { if ("function" == typeof t.test) return t.test(e); if (Array.isArray(t.test)) return t.test.some((function(t) { return e.test(t); })); throw new Error("Browser's test function is not valid"); })); return t && (this.parsedResult.browser = t.describe(this.getUA(), this)), this.parsedResult.browser; }, t.getBrowser = function() { return this.parsedResult.browser ? this.parsedResult.browser : this.parseBrowser(); }, t.getBrowserName = function(e) { return e ? String(this.getBrowser().name).toLowerCase() || "" : this.getBrowser().name || ""; }, t.getBrowserVersion = function() { return this.getBrowser().version; }, t.getOS = function() { return this.parsedResult.os ? this.parsedResult.os : this.parseOS(); }, t.parseOS = function() { var e = this; this.parsedResult.os = {}; var t = s.default.find(n.default, (function(t) { if ("function" == typeof t.test) return t.test(e); if (Array.isArray(t.test)) return t.test.some((function(t) { return e.test(t); })); throw new Error("Browser's test function is not valid"); })); return t && (this.parsedResult.os = t.describe(this.getUA())), this.parsedResult.os; }, t.getOSName = function(e) { var t = this.getOS().name; return e ? String(t).toLowerCase() || "" : t || ""; }, t.getOSVersion = function() { return this.getOS().version; }, t.getPlatform = function() { return this.parsedResult.platform ? this.parsedResult.platform : this.parsePlatform(); }, t.getPlatformType = function(e) { void 0 === e && (e = !1); var t = this.getPlatform().type; return e ? String(t).toLowerCase() || "" : t || ""; }, t.parsePlatform = function() { var e = this; this.parsedResult.platform = {}; var t = s.default.find(a.default, (function(t) { if ("function" == typeof t.test) return t.test(e); if (Array.isArray(t.test)) return t.test.some((function(t) { return e.test(t); })); throw new Error("Browser's test function is not valid"); })); return t && (this.parsedResult.platform = t.describe(this.getUA())), this.parsedResult.platform; }, t.getEngine = function() { return this.parsedResult.engine ? this.parsedResult.engine : this.parseEngine(); }, t.getEngineName = function(e) { return e ? String(this.getEngine().name).toLowerCase() || "" : this.getEngine().name || ""; }, t.parseEngine = function() { var e = this; this.parsedResult.engine = {}; var t = s.default.find(o.default, (function(t) { if ("function" == typeof t.test) return t.test(e); if (Array.isArray(t.test)) return t.test.some((function(t) { return e.test(t); })); throw new Error("Browser's test function is not valid"); })); return t && (this.parsedResult.engine = t.describe(this.getUA())), this.parsedResult.engine; }, t.parse = function() { return this.parseBrowser(), this.parseOS(), this.parsePlatform(), this.parseEngine(), this; }, t.getResult = function() { return s.default.assign({}, this.parsedResult); }, t.satisfies = function(e) { var t = this, r = {}, i = 0, n = {}, a = 0; if (Object.keys(e).forEach((function(t) { var o = e[t]; "string" == typeof o ? (n[t] = o, a += 1) : "object" == typeof o && (r[t] = o, i += 1); })), i > 0) { var o = Object.keys(r), u = s.default.find(o, (function(e) { return t.isOS(e); })); if (u) { var d = this.satisfies(r[u]); if (void 0 !== d) return d; } var c = s.default.find(o, (function(e) { return t.isPlatform(e); })); if (c) { var f = this.satisfies(r[c]); if (void 0 !== f) return f; } } if (a > 0) { var l = Object.keys(n), b = s.default.find(l, (function(e) { return t.isBrowser(e, !0); })); if (void 0 !== b) return this.compareVersion(n[b]); } }, t.isBrowser = function(e, t) { void 0 === t && (t = !1); var r = this.getBrowserName().toLowerCase(), i = e.toLowerCase(), n = s.default.getBrowserTypeByAlias(i); return t && n && (i = n.toLowerCase()), i === r; }, t.compareVersion = function(e) { var t = [0], r = e, i = !1, n = this.getBrowserVersion(); if ("string" == typeof n) return ">" === e[0] || "<" === e[0] ? (r = e.substr(1), "=" === e[1] ? (i = !0, r = e.substr(2)) : t = [], ">" === e[0] ? t.push(1) : t.push(-1)) : "=" === e[0] ? r = e.substr(1) : "~" === e[0] && (i = !0, r = e.substr(1)), t.indexOf(s.default.compareVersions(n, r, i)) > -1; }, t.isOS = function(e) { return this.getOSName(!0) === String(e).toLowerCase(); }, t.isPlatform = function(e) { return this.getPlatformType(!0) === String(e).toLowerCase(); }, t.isEngine = function(e) { return this.getEngineName(!0) === String(e).toLowerCase(); }, t.is = function(e, t) { return void 0 === t && (t = !1), this.isBrowser(e, t) || this.isOS(e) || this.isPlatform(e); }, t.some = function(e) { var t = this; return void 0 === e && (e = []), e.some((function(e) { return t.is(e); })); }, e; }(), e.exports = t.default; }, 92: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i, n = (i = r(17)) && i.__esModule ? i : { default: i }; var a = /version\/(\d+(\.?_?\d+)+)/i; t.default = [ { test: [/gptbot/i], describe: function(e) { var t = { name: "GPTBot" }, r = n.default.getFirstMatch(/gptbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/chatgpt-user/i], describe: function(e) { var t = { name: "ChatGPT-User" }, r = n.default.getFirstMatch(/chatgpt-user\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/oai-searchbot/i], describe: function(e) { var t = { name: "OAI-SearchBot" }, r = n.default.getFirstMatch(/oai-searchbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [ /claudebot/i, /claude-web/i, /claude-user/i, /claude-searchbot/i ], describe: function(e) { var t = { name: "ClaudeBot" }, r = n.default.getFirstMatch(/(?:claudebot|claude-web|claude-user|claude-searchbot)\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/omgilibot/i, /webzio-extended/i], describe: function(e) { var t = { name: "Omgilibot" }, r = n.default.getFirstMatch(/(?:omgilibot|webzio-extended)\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/diffbot/i], describe: function(e) { var t = { name: "Diffbot" }, r = n.default.getFirstMatch(/diffbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/perplexitybot/i], describe: function(e) { var t = { name: "PerplexityBot" }, r = n.default.getFirstMatch(/perplexitybot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/perplexity-user/i], describe: function(e) { var t = { name: "Perplexity-User" }, r = n.default.getFirstMatch(/perplexity-user\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/youbot/i], describe: function(e) { var t = { name: "YouBot" }, r = n.default.getFirstMatch(/youbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/meta-webindexer/i], describe: function(e) { var t = { name: "Meta-WebIndexer" }, r = n.default.getFirstMatch(/meta-webindexer\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/meta-externalads/i], describe: function(e) { var t = { name: "Meta-ExternalAds" }, r = n.default.getFirstMatch(/meta-externalads\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/meta-externalagent/i], describe: function(e) { var t = { name: "Meta-ExternalAgent" }, r = n.default.getFirstMatch(/meta-externalagent\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/meta-externalfetcher/i], describe: function(e) { var t = { name: "Meta-ExternalFetcher" }, r = n.default.getFirstMatch(/meta-externalfetcher\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/googlebot/i], describe: function(e) { var t = { name: "Googlebot" }, r = n.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/linespider/i], describe: function(e) { var t = { name: "Linespider" }, r = n.default.getFirstMatch(/(?:linespider)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/amazonbot/i], describe: function(e) { var t = { name: "AmazonBot" }, r = n.default.getFirstMatch(/amazonbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/bingbot/i], describe: function(e) { var t = { name: "BingCrawler" }, r = n.default.getFirstMatch(/bingbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/baiduspider/i], describe: function(e) { var t = { name: "BaiduSpider" }, r = n.default.getFirstMatch(/baiduspider\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/duckduckbot/i], describe: function(e) { var t = { name: "DuckDuckBot" }, r = n.default.getFirstMatch(/duckduckbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/ia_archiver/i], describe: function(e) { var t = { name: "InternetArchiveCrawler" }, r = n.default.getFirstMatch(/ia_archiver\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/facebookexternalhit/i, /facebookcatalog/i], describe: function() { return { name: "FacebookExternalHit" }; } }, { test: [/slackbot/i, /slack-imgProxy/i], describe: function(e) { var t = { name: "SlackBot" }, r = n.default.getFirstMatch(/(?:slackbot|slack-imgproxy)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/yahoo!?[\s/]*slurp/i], describe: function() { return { name: "YahooSlurp" }; } }, { test: [/yandexbot/i, /yandexmobilebot/i], describe: function() { return { name: "YandexBot" }; } }, { test: [/pingdom/i], describe: function() { return { name: "PingdomBot" }; } }, { test: [/opera/i], describe: function(e) { var t = { name: "Opera" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/opr\/|opios/i], describe: function(e) { var t = { name: "Opera" }, r = n.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/SamsungBrowser/i], describe: function(e) { var t = { name: "Samsung Internet for Android" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/Whale/i], describe: function(e) { var t = { name: "NAVER Whale Browser" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/PaleMoon/i], describe: function(e) { var t = { name: "Pale Moon" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:PaleMoon)[\s/](\d+(?:\.\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/MZBrowser/i], describe: function(e) { var t = { name: "MZ Browser" }, r = n.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/focus/i], describe: function(e) { var t = { name: "Focus" }, r = n.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/swing/i], describe: function(e) { var t = { name: "Swing" }, r = n.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/coast/i], describe: function(e) { var t = { name: "Opera Coast" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/opt\/\d+(?:.?_?\d+)+/i], describe: function(e) { var t = { name: "Opera Touch" }, r = n.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/yabrowser/i], describe: function(e) { var t = { name: "Yandex Browser" }, r = n.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/ucbrowser/i], describe: function(e) { var t = { name: "UC Browser" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/Maxthon|mxios/i], describe: function(e) { var t = { name: "Maxthon" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/epiphany/i], describe: function(e) { var t = { name: "Epiphany" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/puffin/i], describe: function(e) { var t = { name: "Puffin" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/sleipnir/i], describe: function(e) { var t = { name: "Sleipnir" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/k-meleon/i], describe: function(e) { var t = { name: "K-Meleon" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/micromessenger/i], describe: function(e) { var t = { name: "WeChat" }, r = n.default.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/qqbrowser/i], describe: function(e) { var t = { name: /qqbrowserlite/i.test(e) ? "QQ Browser Lite" : "QQ Browser" }, r = n.default.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/msie|trident/i], describe: function(e) { var t = { name: "Internet Explorer" }, r = n.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/\sedg\//i], describe: function(e) { var t = { name: "Microsoft Edge" }, r = n.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/edg([ea]|ios)/i], describe: function(e) { var t = { name: "Microsoft Edge" }, r = n.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/vivaldi/i], describe: function(e) { var t = { name: "Vivaldi" }, r = n.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/seamonkey/i], describe: function(e) { var t = { name: "SeaMonkey" }, r = n.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/sailfish/i], describe: function(e) { var t = { name: "Sailfish" }, r = n.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i, e); return r && (t.version = r), t; } }, { test: [/silk/i], describe: function(e) { var t = { name: "Amazon Silk" }, r = n.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/phantom/i], describe: function(e) { var t = { name: "PhantomJS" }, r = n.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/slimerjs/i], describe: function(e) { var t = { name: "SlimerJS" }, r = n.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/blackberry|\bbb\d+/i, /rim\stablet/i], describe: function(e) { var t = { name: "BlackBerry" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/(web|hpw)[o0]s/i], describe: function(e) { var t = { name: "WebOS Browser" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/bada/i], describe: function(e) { var t = { name: "Bada" }, r = n.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/tizen/i], describe: function(e) { var t = { name: "Tizen" }, r = n.default.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/qupzilla/i], describe: function(e) { var t = { name: "QupZilla" }, r = n.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/librewolf/i], describe: function(e) { var t = { name: "LibreWolf" }, r = n.default.getFirstMatch(/(?:librewolf)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/firefox|iceweasel|fxios/i], describe: function(e) { var t = { name: "Firefox" }, r = n.default.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/electron/i], describe: function(e) { var t = { name: "Electron" }, r = n.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [ /sogoumobilebrowser/i, /metasr/i, /se 2\.[x]/i ], describe: function(e) { var t = { name: "Sogou Browser" }, r = n.default.getFirstMatch(/(?:sogoumobilebrowser)[\s/](\d+(\.?_?\d+)+)/i, e), i = n.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, e), a = n.default.getFirstMatch(/se ([\d.]+)x/i, e), o = r || i || a; return o && (t.version = o), t; } }, { test: [/MiuiBrowser/i], describe: function(e) { var t = { name: "Miui" }, r = n.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: function(e) { return !!e.hasBrand("DuckDuckGo") || e.test(/\sDdg\/[\d.]+$/i); }, describe: function(e, t) { var r = { name: "DuckDuckGo" }; if (t) { var i = t.getBrandVersion("DuckDuckGo"); if (i) return r.version = i, r; } var a = n.default.getFirstMatch(/\sDdg\/([\d.]+)$/i, e); return a && (r.version = a), r; } }, { test: function(e) { return e.hasBrand("Brave"); }, describe: function(e, t) { var r = { name: "Brave" }; if (t) { var i = t.getBrandVersion("Brave"); if (i) return r.version = i, r; } return r; } }, { test: [/chromium/i], describe: function(e) { var t = { name: "Chromium" }, r = n.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/chrome|crios|crmo/i], describe: function(e) { var t = { name: "Chrome" }, r = n.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/GSA/i], describe: function(e) { var t = { name: "Google Search" }, r = n.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: function(e) { var t = !e.test(/like android/i), r = e.test(/android/i); return t && r; }, describe: function(e) { var t = { name: "Android Browser" }, r = n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/playstation 4/i], describe: function(e) { var t = { name: "PlayStation 4" }, r = n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/safari|applewebkit/i], describe: function(e) { var t = { name: "Safari" }, r = n.default.getFirstMatch(a, e); return r && (t.version = r), t; } }, { test: [/.*/i], describe: function(e) { var t = -1 !== e.search("\\(") ? /^(.*)\/(.*)[ \t]\((.*)/ : /^(.*)\/(.*) /; return { name: n.default.getFirstMatch(t, e), version: n.default.getSecondMatch(t, e) }; } } ], e.exports = t.default; }, 93: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i, n = (i = r(17)) && i.__esModule ? i : { default: i }, a = r(18); t.default = [ { test: [/Roku\/DVP/], describe: function(e) { var t = n.default.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i, e); return { name: a.OS_MAP.Roku, version: t }; } }, { test: [/windows phone/i], describe: function(e) { var t = n.default.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i, e); return { name: a.OS_MAP.WindowsPhone, version: t }; } }, { test: [/windows /i], describe: function(e) { var t = n.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i, e), r = n.default.getWindowsVersionName(t); return { name: a.OS_MAP.Windows, version: t, versionName: r }; } }, { test: [/Macintosh(.*?) FxiOS(.*?)\//], describe: function(e) { var t = { name: a.OS_MAP.iOS }, r = n.default.getSecondMatch(/(Version\/)(\d[\d.]+)/, e); return r && (t.version = r), t; } }, { test: [/macintosh/i], describe: function(e) { var t = n.default.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i, e).replace(/[_\s]/g, "."), r = n.default.getMacOSVersionName(t), i = { name: a.OS_MAP.MacOS, version: t }; return r && (i.versionName = r), i; } }, { test: [/(ipod|iphone|ipad)/i], describe: function(e) { var t = n.default.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i, e).replace(/[_\s]/g, "."); return { name: a.OS_MAP.iOS, version: t }; } }, { test: [/OpenHarmony/i], describe: function(e) { var t = n.default.getFirstMatch(/OpenHarmony\s+(\d+(\.\d+)*)/i, e); return { name: a.OS_MAP.HarmonyOS, version: t }; } }, { test: function(e) { var t = !e.test(/like android/i), r = e.test(/android/i); return t && r; }, describe: function(e) { var t = n.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i, e), r = n.default.getAndroidVersionName(t), i = { name: a.OS_MAP.Android, version: t }; return r && (i.versionName = r), i; } }, { test: [/(web|hpw)[o0]s/i], describe: function(e) { var t = n.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i, e), r = { name: a.OS_MAP.WebOS }; return t && t.length && (r.version = t), r; } }, { test: [/blackberry|\bbb\d+/i, /rim\stablet/i], describe: function(e) { var t = n.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i, e) || n.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i, e) || n.default.getFirstMatch(/\bbb(\d+)/i, e); return { name: a.OS_MAP.BlackBerry, version: t }; } }, { test: [/bada/i], describe: function(e) { var t = n.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i, e); return { name: a.OS_MAP.Bada, version: t }; } }, { test: [/tizen/i], describe: function(e) { var t = n.default.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i, e); return { name: a.OS_MAP.Tizen, version: t }; } }, { test: [/linux/i], describe: function() { return { name: a.OS_MAP.Linux }; } }, { test: [/CrOS/], describe: function() { return { name: a.OS_MAP.ChromeOS }; } }, { test: [/PlayStation 4/], describe: function(e) { var t = n.default.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i, e); return { name: a.OS_MAP.PlayStation4, version: t }; } } ], e.exports = t.default; }, 94: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i, n = (i = r(17)) && i.__esModule ? i : { default: i }, a = r(18); t.default = [ { test: [/googlebot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Google" }; } }, { test: [/linespider/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Line" }; } }, { test: [/amazonbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Amazon" }; } }, { test: [/gptbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "OpenAI" }; } }, { test: [/chatgpt-user/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "OpenAI" }; } }, { test: [/oai-searchbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "OpenAI" }; } }, { test: [/baiduspider/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Baidu" }; } }, { test: [/bingbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Bing" }; } }, { test: [/duckduckbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "DuckDuckGo" }; } }, { test: [ /claudebot/i, /claude-web/i, /claude-user/i, /claude-searchbot/i ], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Anthropic" }; } }, { test: [/omgilibot/i, /webzio-extended/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Webz.io" }; } }, { test: [/diffbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Diffbot" }; } }, { test: [/perplexitybot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Perplexity AI" }; } }, { test: [/perplexity-user/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Perplexity AI" }; } }, { test: [/youbot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "You.com" }; } }, { test: [/ia_archiver/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Internet Archive" }; } }, { test: [/meta-webindexer/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Meta" }; } }, { test: [/meta-externalads/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Meta" }; } }, { test: [/meta-externalagent/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Meta" }; } }, { test: [/meta-externalfetcher/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Meta" }; } }, { test: [/facebookexternalhit/i, /facebookcatalog/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Meta" }; } }, { test: [/slackbot/i, /slack-imgProxy/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Slack" }; } }, { test: [/yahoo/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Yahoo" }; } }, { test: [/yandexbot/i, /yandexmobilebot/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Yandex" }; } }, { test: [/pingdom/i], describe: function() { return { type: a.PLATFORMS_MAP.bot, vendor: "Pingdom" }; } }, { test: [/huawei/i], describe: function(e) { var t = n.default.getFirstMatch(/(can-l01)/i, e) && "Nova", r = { type: a.PLATFORMS_MAP.mobile, vendor: "Huawei" }; return t && (r.model = t), r; } }, { test: [/nexus\s*(?:7|8|9|10).*/i], describe: function() { return { type: a.PLATFORMS_MAP.tablet, vendor: "Nexus" }; } }, { test: [/ipad/i], describe: function() { return { type: a.PLATFORMS_MAP.tablet, vendor: "Apple", model: "iPad" }; } }, { test: [/Macintosh(.*?) FxiOS(.*?)\//], describe: function() { return { type: a.PLATFORMS_MAP.tablet, vendor: "Apple", model: "iPad" }; } }, { test: [/kftt build/i], describe: function() { return { type: a.PLATFORMS_MAP.tablet, vendor: "Amazon", model: "Kindle Fire HD 7" }; } }, { test: [/silk/i], describe: function() { return { type: a.PLATFORMS_MAP.tablet, vendor: "Amazon" }; } }, { test: [/tablet(?! pc)/i], describe: function() { return { type: a.PLATFORMS_MAP.tablet }; } }, { test: function(e) { var t = e.test(/ipod|iphone/i), r = e.test(/like (ipod|iphone)/i); return t && !r; }, describe: function(e) { var t = n.default.getFirstMatch(/(ipod|iphone)/i, e); return { type: a.PLATFORMS_MAP.mobile, vendor: "Apple", model: t }; } }, { test: [/nexus\s*[0-6].*/i, /galaxy nexus/i], describe: function() { return { type: a.PLATFORMS_MAP.mobile, vendor: "Nexus" }; } }, { test: [/Nokia/i], describe: function(e) { var t = n.default.getFirstMatch(/Nokia\s+([0-9]+(\.[0-9]+)?)/i, e), r = { type: a.PLATFORMS_MAP.mobile, vendor: "Nokia" }; return t && (r.model = t), r; } }, { test: [/[^-]mobi/i], describe: function() { return { type: a.PLATFORMS_MAP.mobile }; } }, { test: function(e) { return "blackberry" === e.getBrowserName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.mobile, vendor: "BlackBerry" }; } }, { test: function(e) { return "bada" === e.getBrowserName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.mobile }; } }, { test: function(e) { return "windows phone" === e.getBrowserName(); }, describe: function() { return { type: a.PLATFORMS_MAP.mobile, vendor: "Microsoft" }; } }, { test: function(e) { var t = Number(String(e.getOSVersion()).split(".")[0]); return "android" === e.getOSName(!0) && t >= 3; }, describe: function() { return { type: a.PLATFORMS_MAP.tablet }; } }, { test: function(e) { return "android" === e.getOSName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.mobile }; } }, { test: [/smart-?tv|smarttv/i], describe: function() { return { type: a.PLATFORMS_MAP.tv }; } }, { test: [/netcast/i], describe: function() { return { type: a.PLATFORMS_MAP.tv }; } }, { test: function(e) { return "macos" === e.getOSName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.desktop, vendor: "Apple" }; } }, { test: function(e) { return "windows" === e.getOSName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.desktop }; } }, { test: function(e) { return "linux" === e.getOSName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.desktop }; } }, { test: function(e) { return "playstation 4" === e.getOSName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.tv }; } }, { test: function(e) { return "roku" === e.getOSName(!0); }, describe: function() { return { type: a.PLATFORMS_MAP.tv }; } } ], e.exports = t.default; }, 95: function(e, t, r) { "use strict"; t.__esModule = !0, t.default = void 0; var i, n = (i = r(17)) && i.__esModule ? i : { default: i }, a = r(18); t.default = [ { test: function(e) { return "microsoft edge" === e.getBrowserName(!0); }, describe: function(e) { if (/\sedg\//i.test(e)) return { name: a.ENGINE_MAP.Blink }; var t = n.default.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i, e); return { name: a.ENGINE_MAP.EdgeHTML, version: t }; } }, { test: [/trident/i], describe: function(e) { var t = { name: a.ENGINE_MAP.Trident }, r = n.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: function(e) { return e.test(/presto/i); }, describe: function(e) { var t = { name: a.ENGINE_MAP.Presto }, r = n.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: function(e) { var t = e.test(/gecko/i), r = e.test(/like gecko/i); return t && !r; }, describe: function(e) { var t = { name: a.ENGINE_MAP.Gecko }, r = n.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } }, { test: [/(apple)?webkit\/537\.36/i], describe: function() { return { name: a.ENGINE_MAP.Blink }; } }, { test: [/(apple)?webkit/i], describe: function(e) { var t = { name: a.ENGINE_MAP.WebKit }, r = n.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i, e); return r && (t.version = r), t; } } ], e.exports = t.default; } }); })); })))(), 1)).default.getParser(globalThis.navigator.userAgent).getResult(); //#endregion //#region src/utils/number.ts function clampNumber(value, min, max) { if (!Number.isFinite(value)) return min; if (max < min) return min; return Math.max(min, Math.min(max, value)); } function clampNumberWithSortedBounds(value, min, max) { return Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max)); } //#endregion //#region src/utils/localization.ts function getNavigatorLang() { if (typeof navigator === "undefined") return "en"; return navigator.language?.substring(0, 2).toLowerCase() || "en"; } var slavicLangs = /* @__PURE__ */ new Set([ "uk", "be", "bg", "mk", "sr", "bs", "hr", "sl", "pl", "sk", "cs" ]); function resolveCalculatedResLang(baseLang) { if (availableTTS.includes(baseLang)) return baseLang; if (slavicLangs.has(baseLang)) return "ru"; return "en"; } var lang = getNavigatorLang(); var calculatedResLang = resolveCalculatedResLang(lang); //#endregion //#region src/utils/utils.ts var DEFAULT_OBJECT_URL_REVOKE_DELAY_MS = 3e4; var ASCII_CONTROL_CHARS_RE = /\p{Cc}/gu; var INVALID_FILENAME_CHARS_RE = /[\\/:*?"'<>|]+/g; var URL_PROTOCOL_RE = /^https?:\/\//i; var MULTIPLE_DASHES_RE = /-{2,}/g; var FILENAME_EDGE_WHITESPACE_RE = /^\s$/u; var isFilenameEdgeChar = (char) => char === "." || char === "-" || FILENAME_EDGE_WHITESPACE_RE.test(char); var trimFilenameEdgeChars = (value) => { let startIndex = 0; let endIndex = value.length; while (startIndex < endIndex && isFilenameEdgeChar(value[startIndex])) startIndex += 1; while (endIndex > startIndex && isFilenameEdgeChar(value[endIndex - 1])) endIndex -= 1; return value.slice(startIndex, endIndex); }; function getDateFallbackFilename() { return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10); } function stripAsciiControlChars(value) { return value.replace(ASCII_CONTROL_CHARS_RE, ""); } /** * Creates a stable JSON string representation for consistent hashing * @param value The value to stringify * @returns A stable JSON string */ function stableStringify(value) { const seen = /* @__PURE__ */ new WeakSet(); return JSON.stringify(value, (_key, val) => { if (val && typeof val === "object") { if (seen.has(val)) return "[Circular]"; seen.add(val); if (Array.isArray(val)) return val; const sorted = {}; const keys = Object.keys(val).sort((a, b) => a.localeCompare(b)); for (const key of keys) sorted[key] = val[key]; return sorted; } return val; }); } /** * Small, deterministic hash for cache keys. (Not crypto.) * @param str The string to hash * @returns A base36 string representation of the hash */ function fnv1a32ToKeyPart(str) { let hash = 2166136261; let i = 0; while (i < str.length) { const codePoint = str.codePointAt(i) ?? 0; hash ^= codePoint; hash = Math.imul(hash, 16777619); i += codePoint > 65535 ? 2 : 1; } return (hash >>> 0).toString(36); } var isPiPAvailable = () => Boolean(document.pictureInPictureEnabled); async function writeBlobToHandle(handle, blob) { try { const writable = await handle.createWritable(); await writable.write(blob); await writable.close(); return true; } catch { return false; } } async function shareBlob(blob, filename) { const nav = typeof navigator === "undefined" ? void 0 : navigator; if (!nav?.share || typeof File === "undefined") return "unsupported"; let file; try { file = new File([blob], filename, { type: blob.type || "application/octet-stream" }); } catch { return "unsupported"; } if (typeof nav.canShare === "function" && !nav.canShare({ files: [file] })) return "unsupported"; try { await nav.share({ files: [file], title: filename }); return "shared"; } catch (err) { if (err instanceof DOMException && err.name === "AbortError") return "shared"; return "error"; } } function triggerBlobDownload(blob, filename) { const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.rel = "noopener noreferrer"; anchor.target = "_blank"; anchor.style.position = "fixed"; anchor.style.left = "-9999px"; anchor.style.top = "0"; (document.body ?? document.documentElement).append(anchor); try { anchor.click(); return true; } catch { return false; } finally { anchor.remove(); revokeObjectUrlLater(url); } } /** Downloads binary file with entered filename */ async function downloadBlob(blob, filename, options = {}) { if (options.fileHandle) { if (await writeBlobToHandle(options.fileHandle, blob)) return true; } if (options.preferShare) { if (await shareBlob(blob, filename) === "shared") return true; } return triggerBlobDownload(blob, filename); } function revokeObjectUrlLater(url, delayMs = DEFAULT_OBJECT_URL_REVOKE_DELAY_MS) { const safeDelayMs = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : DEFAULT_OBJECT_URL_REVOKE_DELAY_MS; globalThis.setTimeout(() => URL.revokeObjectURL(url), safeDelayMs); } function clearFileName(filename) { const trimmed = filename.trim(); if (!trimmed) return getDateFallbackFilename(); return trimFilenameEdgeChars(stripAsciiControlChars(trimmed).replace(URL_PROTOCOL_RE, "").replace(INVALID_FILENAME_CHARS_RE, "-").replace(MULTIPLE_DASHES_RE, "-")) || getDateFallbackFilename(); } var getTimestamp = () => Math.floor(Date.now() / 1e3); var getHeaders = (headers) => headers ? Object.fromEntries(new Headers(headers)) : {}; function clamp(value, min = 0, max = 100) { return clampNumberWithSortedBounds(value, min, max); } function toFlatObj(data) { const out = {}; const stack = Object.entries(data); while (stack.length) { const entry = stack.pop(); if (!entry) continue; const [key, val] = entry; if (val === void 0) continue; if (!(val !== null && typeof val === "object" && !Array.isArray(val))) { out[key] = val; continue; } for (const [k, v] of Object.entries(val)) stack.push([`${key}.${k}`, v]); } return out; } //#endregion //#region src/utils/responseCache.ts var RESPONSE_CACHE_CREATED_AT_HEADER = "x-vot-cache-created-at"; var RESPONSE_CACHE_KEY_HEADER = "x-vot-cache-key"; var DEFAULT_RESPONSE_CACHE_NAME = "vot-http-cache-v1"; function computeExpiresAt(createdAtMs, ttlMs) { if (!Number.isFinite(ttlMs) || ttlMs <= 0) return createdAtMs; return ttlMs >= Number.MAX_SAFE_INTEGER - createdAtMs ? Number.MAX_SAFE_INTEGER : createdAtMs + ttlMs; } function normalizeMethod(method) { return (method || "GET").toUpperCase(); } function resolveBodyKey(body) { if (body == null) return ""; if (typeof body === "string") return body; if (body instanceof URLSearchParams) return body.toString(); } var ResponseCacheManager = class { inFlightRequests = /* @__PURE__ */ new Map(); async execute(context, options, fetcher) { if (!options || options.ttlMs <= 0) return fetcher(); const method = normalizeMethod(context.method); const key = options.key ?? this.buildDefaultCacheKey(context); if (!key) return fetcher(); const ttlMs = options.ttlMs; const cacheName = options.cacheName || DEFAULT_RESPONSE_CACHE_NAME; const useCacheApi = options.useCacheApi !== false && method === "GET" && this.supportsCacheApi(); const cacheApiKey = useCacheApi ? fnv1a32ToKeyPart(key) : ""; const dedupe = options.dedupe !== false; const allowStaleOnError = options.allowStaleOnError !== false; const cached = useCacheApi ? await this.readCacheApi(cacheName, context.url, cacheApiKey, ttlMs, Date.now(), allowStaleOnError) : {}; if (cached.fresh) return cached.fresh; const networkRequest = () => this.runNetworkRequestWithFallback({ cacheName, url: context.url, cacheApiKey, useCacheApi }, fetcher, allowStaleOnError ? cached.stale : void 0); if (!dedupe) return await networkRequest(); const inFlight = this.inFlightRequests.get(key); if (inFlight !== void 0) return (await inFlight).clone(); const networkPromise = networkRequest(); this.inFlightRequests.set(key, networkPromise); try { return (await networkPromise).clone(); } finally { this.inFlightRequests.delete(key); } } async runNetworkRequestWithFallback(cacheConfig, fetcher, staleFallback) { let response; try { response = await fetcher(); } catch (err) { if (staleFallback) return staleFallback; throw err; } if (!response.ok) return response; if (cacheConfig.useCacheApi) { const createdAtMs = Date.now(); const storable = this.toStorableResponse(response.clone(), createdAtMs); await this.writeCacheApi(cacheConfig.cacheName, cacheConfig.url, cacheConfig.cacheApiKey, storable); } return response; } buildDefaultCacheKey(context) { const method = normalizeMethod(context.method); if (method === "GET") return `${method}:${context.url}`; const bodyKey = resolveBodyKey(context.body); if (bodyKey === void 0) return void 0; return `${method}:${context.url}#${fnv1a32ToKeyPart(bodyKey)}`; } supportsCacheApi() { return typeof caches !== "undefined" && typeof caches.open === "function"; } readCreatedAtMs(response) { const raw = response.headers.get(RESPONSE_CACHE_CREATED_AT_HEADER); if (!raw) return null; const value = Number(raw); return Number.isFinite(value) ? value : null; } ensureVaryByCacheKey(headers) { const varyRaw = headers.get("vary"); if (!varyRaw) { headers.set("vary", RESPONSE_CACHE_KEY_HEADER); return; } const varyParts = new Set(varyRaw.split(",").map((part) => part.trim().toLowerCase())); if (!varyParts.has("*") && !varyParts.has(RESPONSE_CACHE_KEY_HEADER)) headers.set("vary", `${varyRaw}, ${RESPONSE_CACHE_KEY_HEADER}`); } toStorableResponse(response, createdAtMs) { const headers = new Headers(response.headers); headers.set(RESPONSE_CACHE_CREATED_AT_HEADER, String(createdAtMs)); this.ensureVaryByCacheKey(headers); return new Response(response.body, { status: response.status, statusText: response.statusText, headers }); } async readCacheApi(cacheName, url, cacheKey, ttlMs, nowMs, allowStaleOnError) { try { const request = new Request(url, { method: "GET", headers: { [RESPONSE_CACHE_KEY_HEADER]: cacheKey } }); const cache = await caches.open(cacheName); const cached = await cache.match(request); if (!cached) return {}; const createdAtMs = this.readCreatedAtMs(cached); if (createdAtMs === null) { await cache.delete(request); return {}; } const expiresAt = computeExpiresAt(createdAtMs, ttlMs); if (expiresAt > nowMs) return { fresh: cached.clone(), expiresAt }; if (!allowStaleOnError) { await cache.delete(request); return {}; } const stale = cached.clone(); await cache.delete(request); return { stale, expiresAt }; } catch { return {}; } } async writeCacheApi(cacheName, url, cacheKey, response) { try { const request = new Request(url, { method: "GET", headers: { [RESPONSE_CACHE_KEY_HEADER]: cacheKey } }); await (await caches.open(cacheName)).put(request, response); } catch {} } }; var responseCacheManager = new ResponseCacheManager(); async function executeWithResponseCache(context, options, fetcher) { return responseCacheManager.execute(context, options, fetcher); } //#endregion //#region src/utils/gm.ts var YANDEX_API_HOST = "api.browser.yandex.ru"; var GOOGLEVIDEO_HOST_SUFFIX = "googlevideo.com"; var HEADER_LINE_RE = /^(\w[\w-]*):\s*(\S.*)$/; var URL_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/; var scriptHandler = typeof GM_info === "undefined" ? void 0 : GM_info?.scriptHandler; function getCallbackGmXhr() { const gmXhr = typeof GM_xmlhttpRequest === "undefined" ? globalThis.GM_xmlhttpRequest : GM_xmlhttpRequest; return typeof gmXhr === "function" ? gmXhr : void 0; } function getPromiseGmXhr() { const gm = typeof GM === "undefined" ? globalThis.GM : GM; const gmXhr = gm?.xmlHttpRequest ?? gm?.xmlhttpRequest; return typeof gmXhr === "function" ? gmXhr.bind(gm) : void 0; } function hasSupportedGmXhr() { return !!(getCallbackGmXhr() || getPromiseGmXhr()); } var isProxyOnlyExtension = browserInfo.browser?.name === "Safari" || !["Tampermonkey", "Violentmonkey"].includes(scriptHandler); /** * Returns true when the GM4 promise-based API is available. * * Safe to read as a module-level const because: * - CRXJS Chrome: the IIFE prelude installs GM globals synchronously at * document_start, before this module evaluates at document_end. * - Firefox: the bridge injects prelude.module.js before content.module.js. * - Userscript managers inject GM before the script runs. */ var isGM4Supported = typeof GM !== "undefined" || globalThis.GM !== void 0; var isSupportGMXhr = hasSupportedGmXhr(); function getRequestHost(url) { const normalizedUrl = url.trim(); try { return new URL(normalizedUrl).hostname.toLowerCase(); } catch { if (!URL_SCHEME_RE.test(normalizedUrl)) try { return new URL(`https://${normalizedUrl}`).hostname.toLowerCase(); } catch {} return; } } function isHostOrSubdomain(host, targetHost) { return host === targetHost || host.endsWith(`.${targetHost}`); } function shouldUseGmXhr(host, url, forceGmXhr = false) { if (forceGmXhr) return true; if (!host) { const lowerUrl = url.toLowerCase(); return lowerUrl.includes(YANDEX_API_HOST) || lowerUrl.includes(GOOGLEVIDEO_HOST_SUFFIX); } return isHostOrSubdomain(host, YANDEX_API_HOST) || isHostOrSubdomain(host, GOOGLEVIDEO_HOST_SUFFIX); } function toRequestUrl(url) { if (typeof url === "string") return url; if (url instanceof URL) return url.href; return url.url; } function resolveRequestMethod(url, method) { if (method) return method.toUpperCase(); if (url instanceof Request) return (url.method || "GET").toUpperCase(); return "GET"; } function parseResponseHeaders(rawHeaders) { if (typeof rawHeaders !== "string" || rawHeaders.length === 0) return {}; return rawHeaders.split(/\r?\n/).reduce((acc, line) => { const headerMatch = HEADER_LINE_RE.exec(line); if (!headerMatch) return acc; const [, key, value] = headerMatch; acc[key] = value; return acc; }, {}); } function getGmXhrErrorMessage(error) { const maybeError = error; if (typeof maybeError?.error === "string" && maybeError.error.trim().length > 0) return maybeError.error; if (typeof maybeError?.statusText === "string" && maybeError.statusText.trim().length > 0 && maybeError.statusText !== "\"\"" && maybeError.statusText !== "''") return maybeError.statusText; return getErrorMessage(error) || "Unknown GM XHR error"; } function buildResponse(resp, urlStr) { const status = resp.status; const statusText = typeof resp.statusText === "string" ? resp.statusText : ""; const body = resp.response instanceof Blob ? resp.response : null; const responseHeaders = parseResponseHeaders(resp.responseHeaders); const response = new Response(body, { status, statusText, headers: responseHeaders }); Object.defineProperty(response, "url", { value: resp.finalUrl ?? urlStr }); return response; } async function executeCallbackGmXhr(gmXhr, urlStr, timeout, fetchOptions, method, headers) { return new Promise((resolve, reject) => { let settled = false; let onAbort; const cleanupAbort = () => { if (onAbort) fetchOptions.signal?.removeEventListener("abort", onAbort); }; const failOnce = (error) => { if (settled) return; settled = true; cleanupAbort(); reject(error); }; const redirectMode = fetchOptions.redirect; const request = gmXhr({ method, url: urlStr, responseType: "blob", data: fetchOptions.body, timeout, headers, ...redirectMode && { redirect: redirectMode }, onload: (resp) => { if (settled) return; settled = true; cleanupAbort(); try { const response = buildResponse(resp, urlStr); debug.log("[GM_fetch] GM_xmlhttpRequest completed", { url: response.url, method, status: response.status, statusText: response.statusText }); resolve(response); } catch (buildErr) { debug.warn("[GM_fetch] GM_xmlhttpRequest response build failed", { url: urlStr, method, error: getErrorMessage(buildErr), rawStatus: resp.status, rawStatusText: resp.statusText }); reject(buildErr instanceof Error ? buildErr : new Error(getErrorMessage(buildErr))); } }, ontimeout: () => { debug.warn("[GM_fetch] GM_xmlhttpRequest timed out", { url: urlStr, method, timeout }); failOnce(/* @__PURE__ */ new Error("Timeout")); }, onerror: (error) => { const message = getGmXhrErrorMessage(error); debug.warn("[GM_fetch] GM_xmlhttpRequest failed", { url: urlStr, method, error: message }); failOnce(new Error(message)); }, onabort: () => { debug.warn("[GM_fetch] GM_xmlhttpRequest aborted", { url: urlStr, method }); failOnce(makeAbortError()); } }); onAbort = () => { try { request?.abort?.(); } catch {} failOnce(makeAbortError()); }; if (fetchOptions.signal) { fetchOptions.signal.addEventListener("abort", onAbort, { once: true }); if (fetchOptions.signal.aborted) { onAbort(); return; } } }); } async function executePromiseGmXhr(gmXhr, urlStr, timeout, fetchOptions, method, headers) { const redirectMode = fetchOptions.redirect; const request = gmXhr({ method, url: urlStr, responseType: "blob", data: fetchOptions.body, timeout, headers, ...redirectMode && { redirect: redirectMode } }); let abortHandler; try { const abortPromise = new Promise((_, reject) => { if (!fetchOptions.signal) return; abortHandler = () => { try { request.abort?.(); } catch {} reject(makeAbortError()); }; fetchOptions.signal.addEventListener("abort", abortHandler, { once: true }); if (fetchOptions.signal.aborted) abortHandler(); }); const response = buildResponse(await Promise.race([request, abortPromise]), urlStr); debug.log("[GM_fetch] GM.xmlHttpRequest completed", { url: response.url, method, status: response.status, statusText: response.statusText }); return response; } finally { if (abortHandler) fetchOptions.signal?.removeEventListener("abort", abortHandler); } } async function gmXhrFetch(urlStr, timeout, fetchOptions) { const headers = getHeaders(fetchOptions.headers); const method = (fetchOptions.method || "GET").toUpperCase(); debug.log("[GM_fetch] GM_xmlhttpRequest start", { url: urlStr, method, timeout, headerCount: Object.keys(headers).length }); const callbackGmXhr = getCallbackGmXhr(); if (callbackGmXhr) { debug.log("[GM_fetch] attempting callback-style GM_xmlhttpRequest"); try { return await executeCallbackGmXhr(callbackGmXhr, urlStr, timeout, fetchOptions, method, headers); } catch (error) { if (isAbortError(error)) throw error; debug.warn("[GM_fetch] callback-style GM_xmlhttpRequest failed", { url: urlStr, method, error: getGmXhrErrorMessage(error) }); } } const promiseGmXhr = getPromiseGmXhr(); if (promiseGmXhr) { debug.log("[GM_fetch] attempting promise-style GM.xmlHttpRequest"); try { return await executePromiseGmXhr(promiseGmXhr, urlStr, timeout, fetchOptions, method, headers); } catch (error) { if (isAbortError(error)) throw error; debug.warn("[GM_fetch] promise-style GM.xmlHttpRequest failed", { url: urlStr, method, error: getGmXhrErrorMessage(error) }); } } debug.warn("[GM_fetch] none of the GM approaches worked"); throw new Error("All GM approaches failed"); } async function GM_fetch(url, opts = {}) { const { timeout = 15e3, forceGmXhr = false, responseCache, ...fetchOptions } = opts; const urlStr = toRequestUrl(url); const host = getRequestHost(urlStr); const method = resolveRequestMethod(url, fetchOptions.method); const useGmXhr = shouldUseGmXhr(host, urlStr, forceGmXhr); debug.log("[GM_fetch] request", { url: urlStr, method, host: host ?? "unknown", timeout, transport: useGmXhr ? "GM_xmlhttpRequest" : "fetch", forced: forceGmXhr, responseCache: responseCache ? { ttlMs: responseCache.ttlMs, key: responseCache.key ?? null, useCacheApi: responseCache.useCacheApi ?? true, dedupe: responseCache.dedupe ?? true, allowStaleOnError: responseCache.allowStaleOnError ?? true } : null }); const performRequest = async () => { if (useGmXhr) { debug.log("[GM_fetch] using GM_xmlhttpRequest transport", { url: urlStr, method, host: host ?? "unknown", reason: forceGmXhr ? "forced" : "host-policy" }); try { return await gmXhrFetch(urlStr, timeout, fetchOptions); } catch (err) { if (isAbortError(err)) throw err; if (forceGmXhr || shouldUseGmXhr(host, urlStr)) throw err; debug.warn("[GM_fetch] all GM approaches failed, falling back to native fetch", { url: urlStr, method, host: host ?? "unknown", error: getErrorMessage(err) || "Unknown error" }); const { signal, cleanup } = createTimeoutSignal(timeout, fetchOptions.signal); try { return await fetch(url, { ...fetchOptions, signal }); } finally { cleanup(); } } } const { signal, cleanup } = createTimeoutSignal(timeout, fetchOptions.signal); try { return await fetch(url, { ...fetchOptions, signal }); } catch (err) { if (signal.aborted || isAbortError(err)) throw err; debug.warn("[GM_fetch] fetch failed, retrying via GM_xmlhttpRequest", { url: urlStr, method, host: host ?? "unknown", error: getErrorMessage(err) || "Unknown error" }); return await gmXhrFetch(urlStr, timeout, fetchOptions); } finally { cleanup(); } }; if (!responseCache) return await performRequest(); return await executeWithResponseCache({ url: urlStr, method, body: fetchOptions.body }, responseCache, performRequest); } //#endregion //#region src/utils/storage.ts var compatRules = Object.entries({ numToBool: [ ["autoTranslate"], ["dontTranslateYourLang", "enabledDontTranslateLanguages"], ["autoSetVolumeYandexStyle", "enabledAutoVolume"], ["showVideoSlider"], ["syncVolume"], ["downloadWithName"], ["sendNotifyOnComplete"], ["highlightWords"], ["onlyBypassMediaCSP"], ["newAudioPlayer"], ["showPiPButton"], ["translateAPIErrors"], ["audioBooster"], ["useNewModel", "useLivelyVoice"] ], number: [["autoVolume"]], array: [["dontTranslateLanguage", "dontTranslateLanguages"]], string: [ ["hotkeyButton", "translationHotkey"], ["locale-lang-override", "localeLangOverride"], ["locale-lang", "localeLang"] ] }).flatMap(([category, entries]) => entries.map(([oldKey, maybeNewKey]) => ({ category, oldKey, newKey: maybeNewKey ?? oldKey, shouldDeleteOldKey: Boolean(maybeNewKey) }))); var compatRuleByOldKey = new Map(compatRules.map((rule) => [rule.oldKey, rule])); var compatKeysToRead = Array.from(new Set(compatRules.map((rule) => rule.oldKey))); function createUndefinedDefaults(keys) { const defaults = {}; for (const key of keys) defaults[key] = void 0; return defaults; } function isCompatValue(category, value) { switch (category) { case "numToBool": case "number": return typeof value === "number"; case "array": return Array.isArray(value); case "string": return typeof value === "string" || value === null; default: return false; } } function convertByCompatCategory(category, value) { switch (category) { case "string": case "array": case "number": return value; default: return !!value; } } function normalizeCompatValue(rule, value) { let convertedValue = convertByCompatCategory(rule.category, value); if (rule.oldKey === "autoVolume" && typeof value === "number" && value < 1) convertedValue = Math.round(value * 100); return convertedValue; } function areStorageValuesEqual(a, b) { if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => Object.is(item, b[index])); return Object.is(a, b); } function parseStoredValue(rawValue) { if (rawValue === null) return; try { return JSON.parse(rawValue); } catch { return; } } async function updateConfig(data) { if (data.compatVersion === "2025-05-09") return data; const keysToRead = /* @__PURE__ */ new Set([...Object.keys(data), ...compatKeysToRead]); const persistedValues = await votStorage.getValues(createUndefinedDefaults(keysToRead)); const newData = { ...data }; const writeOperations = []; const deleteOperations = []; for (const [key, storedValue] of Object.entries(persistedValues)) { if (storedValue === void 0) continue; const compatRule = compatRuleByOldKey.get(key); if (!compatRule || !isCompatValue(compatRule.category, storedValue)) continue; const convertedValue = normalizeCompatValue(compatRule, storedValue); newData[compatRule.newKey] = convertedValue; const existingNewValue = persistedValues[compatRule.newKey]; if (compatRule.shouldDeleteOldKey || !areStorageValuesEqual(existingNewValue, convertedValue)) writeOperations.push(votStorage.set(compatRule.newKey, convertedValue)); if (compatRule.shouldDeleteOldKey) deleteOperations.push(votStorage.delete(compatRule.oldKey)); } await Promise.all([...writeOperations, ...deleteOperations]); return { ...newData, compatVersion: actualCompatVersion }; } var VOTStorage = class { support = null; localStorageListeners = /* @__PURE__ */ new Map(); shouldUseSyntheticListeners(support) { return !support.promiseAddValueChangeListener && !support.legacyAddValueChangeListener; } getGMRuntime() { if (typeof GM !== "undefined") return GM; return globalThis.GM; } resolveSupport() { if (this.support) return this.support; const gm = this.getGMRuntime(); const support = { legacyGet: typeof GM_getValue === "function", legacySet: typeof GM_setValue === "function", legacyDelete: typeof GM_deleteValue === "function", legacyList: typeof GM_listValues === "function", legacyAddValueChangeListener: typeof globalThis.GM_addValueChangeListener === "function", legacyRemoveValueChangeListener: typeof globalThis.GM_removeValueChangeListener === "function", promiseGet: isGM4Supported && typeof gm?.getValue === "function", promiseGetValues: isGM4Supported && typeof gm?.getValues === "function", promiseSet: isGM4Supported && typeof gm?.setValue === "function", promiseDelete: isGM4Supported && typeof gm?.deleteValue === "function", promiseList: isGM4Supported && typeof gm?.listValues === "function", promiseAddValueChangeListener: isGM4Supported && typeof gm?.addValueChangeListener === "function", promiseRemoveValueChangeListener: isGM4Supported && typeof gm?.removeValueChangeListener === "function" }; this.support = support; debug.log(`[VOT Storage] GM Promises: ${support.promiseGet} | GM legacy: ${support.legacyGet}`); return support; } /** * Check if storage type is LocalStorage */ get isSupportOnlyLS() { const support = this.resolveSupport(); return !support.legacyGet && !support.legacySet && !support.legacyDelete && !support.legacyList && !support.promiseGet && !support.promiseGetValues && !support.promiseSet && !support.promiseDelete && !support.promiseList; } syncGetByName(name, def, support) { if (support.legacyGet) return GM_getValue(name, def); const val = globalThis.localStorage.getItem(name); if (val === null) return def; try { return JSON.parse(val); } catch { return def; } } async getRaw(name, def) { const support = this.resolveSupport(); if (support.promiseGet && GM.getValue) return await GM.getValue(name, def); return this.syncGetByName(name, def, support); } async get(name, def) { return this.getRaw(name, def); } async getValues(data) { const support = this.resolveSupport(); if (support.promiseGetValues && GM.getValues) return await GM.getValues(data); const entries = Object.entries(data); if (support.promiseGet && GM.getValue) { const values = await Promise.all(entries.map(async ([key, value]) => { return [key, await GM.getValue(key, value)]; })); return Object.fromEntries(values); } return Object.fromEntries(entries.map(([key, value]) => [key, this.syncGetByName(key, value, support)])); } syncSetByName(name, value, support) { if (support.legacySet) return GM_setValue(name, value); return globalThis.localStorage.setItem(name, JSON.stringify(value)); } async setRaw(name, value) { const support = this.resolveSupport(); const storageKey = name; const shouldNotify = this.shouldUseSyntheticListeners(support); const oldValue = shouldNotify ? await this.getRaw(name) : void 0; if (support.promiseSet && GM.setValue) { await GM.setValue(name, value); if (shouldNotify) this.notifyLocalStorageListeners(storageKey, oldValue, value, false); return; } const setResult = this.syncSetByName(name, value, support); this.notifyLocalStorageListeners(storageKey, oldValue, value, false); return setResult; } async set(name, value) { return this.setRaw(name, value); } syncDeleteByName(name, support) { if (support.legacyDelete) return GM_deleteValue(name); return globalThis.localStorage.removeItem(name); } async deleteRaw(name) { const support = this.resolveSupport(); const storageKey = name; const shouldNotify = this.shouldUseSyntheticListeners(support); const oldValue = shouldNotify ? await this.getRaw(name) : void 0; if (support.promiseDelete && GM.deleteValue) { await GM.deleteValue(name); if (shouldNotify) this.notifyLocalStorageListeners(storageKey, oldValue, void 0, false); return; } const deleteResult = this.syncDeleteByName(name, support); this.notifyLocalStorageListeners(storageKey, oldValue, void 0, false); return deleteResult; } async delete(name) { return this.deleteRaw(name); } addValueChangeListener(name, listener) { const support = this.resolveSupport(); const gm = this.getGMRuntime(); if (support.promiseAddValueChangeListener) { const addListener = gm?.addValueChangeListener; const removeListener = support.promiseRemoveValueChangeListener ? gm?.removeValueChangeListener : void 0; if (typeof addListener === "function") { const listenerId = addListener(name, this.createTypedListener(listener)); return () => { if (typeof removeListener === "function") removeListener(listenerId); }; } } if (support.legacyAddValueChangeListener) { const addListener = globalThis.GM_addValueChangeListener; const removeListener = support.legacyRemoveValueChangeListener ? globalThis.GM_removeValueChangeListener : void 0; if (typeof addListener === "function") { const listenerId = addListener(name, this.createTypedListener(listener)); return () => { if (typeof removeListener === "function") removeListener(listenerId); }; } } const listeners = this.getLocalStorageListeners(name); const typedListener = listener; listeners.add(typedListener); const onStorage = (event) => { if (event.storageArea !== globalThis.localStorage || event.key !== name) return; typedListener(name, parseStoredValue(event.oldValue), parseStoredValue(event.newValue), true); }; globalThis.addEventListener("storage", onStorage); return () => { listeners.delete(typedListener); if (listeners.size === 0) this.localStorageListeners.delete(name); globalThis.removeEventListener("storage", onStorage); }; } createTypedListener(listener) { return (key, oldValue, newValue, remote) => { listener(key, oldValue, newValue, remote); }; } getLocalStorageListeners(name) { const existing = this.localStorageListeners.get(name); if (existing) return existing; const created = /* @__PURE__ */ new Set(); this.localStorageListeners.set(name, created); return created; } notifyLocalStorageListeners(name, oldValue, newValue, remote) { const listeners = this.localStorageListeners.get(name); if (!listeners || listeners.size === 0) return; for (const listener of listeners) listener(name, oldValue, newValue, remote); } syncList(support) { if (support.legacyList) return GM_listValues(); return storageKeys; } async list() { const support = this.resolveSupport(); if (support.promiseList && GM.listValues) return await GM.listValues(); return this.syncList(support); } }; var VOT_STORAGE_GLOBAL_KEY = "__VOT_STORAGE_SINGLETON__"; var votStorage = (() => { const scope = globalThis; const existing = scope[VOT_STORAGE_GLOBAL_KEY]; if (existing instanceof VOTStorage) return existing; const created = new VOTStorage(); scope[VOT_STORAGE_GLOBAL_KEY] = created; return created; })(); //#endregion //#region src/core/authRefreshMessage.ts var AUTH_REFRESH_MESSAGE_SOURCE = "vot-auth"; var AUTH_REFRESH_MESSAGE_TYPE = "account-updated"; function createAuthRefreshMessage() { return { source: AUTH_REFRESH_MESSAGE_SOURCE, type: AUTH_REFRESH_MESSAGE_TYPE }; } function isAuthRefreshMessage(value) { if (!value || typeof value !== "object") return false; const candidate = value; return candidate.source === "vot-auth" && candidate.type === "account-updated"; } function notifyAuthOpener(target = globalThis.opener) { if (!target || typeof target.postMessage !== "function") return; target.postMessage(createAuthRefreshMessage(), globalThis.location.origin); } //#endregion //#region src/core/auth.ts function getProfilePayload() { const payload = globalThis._userData; if (!payload || typeof payload !== "object") return null; const candidate = payload; if (typeof candidate.avatar_id !== "string" || typeof candidate.username !== "string" || candidate.avatar_id.length === 0 || candidate.username.length === 0) return null; return { avatar_id: candidate.avatar_id, username: candidate.username }; } async function handleAuthCallbackPage() { const { access_token: token, expires_in: expiresIn } = Object.fromEntries(new URLSearchParams(globalThis.location.hash.slice(1))); if (!token || !expiresIn) throw new Error("[VOT] Invalid token response"); const numExpiresIn = Number.parseInt(expiresIn, 10); if (Number.isNaN(numExpiresIn)) throw new TypeError("[VOT] Invalid expires_in value"); await votStorage.set("account", { token, expires: Date.now() + numExpiresIn * 1e3, username: void 0, avatarId: void 0 }); notifyAuthOpener(); } async function handleProfilePage() { const payload = getProfilePayload(); if (!payload) throw new Error("[VOT] Invalid user data"); const { avatar_id: avatarId, username } = payload; const data = await votStorage.get("account"); if (!data) throw new Error("[VOT] No account data found"); await votStorage.set("account", { ...data, username, avatarId }); notifyAuthOpener(); } async function initAuth() { if (globalThis.location.pathname === "/auth/callback") return handleAuthCallbackPage(); if (globalThis.location.pathname === "/my/profile") return handleProfilePage(); } var en_default = { recommended: "recommended", translateVideo: "Translate video", disableTranslate: "Turn off", translationSettings: "Translation settings", subtitlesSettings: "Subtitles settings", subtitlesSmartLayout: "Smart subtitle layout", resetSettings: "Reset settings", videoBeingTranslated: "The video is being translated", videoLanguage: "Video language", translationLanguage: "Translation language", translationTake: "The translation will take", translationTakeMoreThanHour: "The translation will take more than an hour", translationTakeAboutMinute: "The translation will take about a minute", translationTakeFewMinutes: "The translation will take a few minutes", translationTakeApproximatelyMinutes: "The translation will take approximately {0} minutes", translationTakeApproximatelyMinute: "The translation will take approximately {0} minutes", requestTranslationFailed: "Failed to request video translation", audioNotReceived: "Audio link not received", VOTYouTubeSignInSuggested: "Sign in to YouTube and try translation again", VOTFailedDownloadAudio: "Failed to download audio", audioFormatNotSupported: "The audio format is not supported", VOTAutoTranslate: "Translate on open", VOTAutoSubtitles: "Subtitles on open", VOTDontTranslateYourLang: "Don't translate from my language", VOTVolume: "Video volume:", VOTVolumeTranslation: "Translation volume:", VOTAutoSetVolume: "Reduce video volume to", VOTShowVideoSlider: "Video volume slider", VOTSyncVolume: "Link translation and video volume", VOTDisableFromYourLang: "You have disabled the translation of the video in your language", VOTVideoIsTooLong: "Video is too long", VOTNoVideoIDFound: "No video ID found", VOTSubtitles: "Subtitles", VOTSubtitlesDisabled: "Disabled", VOTDefaultSubtitlesLanguage: "Default subtitle language", VOTOriginalVideoLanguage: "Original video language", VOTSubtitlesMaxLength: "Subtitles max length", VOTHighlightWords: "Highlight words", VOTTranslatedFrom: "translated from", VOTAutogenerated: "autogenerated", VOTSettings: "VOT Settings", VOTMenuLanguage: "Menu language", VOTAuthors: "Authors", VOTVersion: "Version", VOTLoader: "Loader", VOTBrowser: "Browser", VOTShowPiPButton: "Show PiP button", langs: { "auto": "Auto", "af": "Afrikaans", "ak": "Akan", "sq": "Albanian", "am": "Amharic", "ar": "Arabic", "hy": "Armenian", "as": "Assamese", "ay": "Aymara", "az": "Azerbaijani", "bn": "Bangla", "eu": "Basque", "be": "Belarusian", "bho": "Bhojpuri", "bs": "Bosnian", "bg": "Bulgarian", "my": "Burmese", "ca": "Catalan", "ceb": "Cebuano", "zh": "Chinese", "zh-Hans": "Chinese (Simplified)", "zh-Hant": "Chinese (Traditional)", "co": "Corsican", "hr": "Croatian", "cs": "Czech", "da": "Danish", "dv": "Divehi", "nl": "Dutch", "en": "English", "eo": "Esperanto", "et": "Estonian", "ee": "Ewe", "fil": "Filipino", "fi": "Finnish", "fr": "French", "gl": "Galician", "lg": "Ganda", "ka": "Georgian", "de": "German", "el": "Greek", "gn": "Guarani", "gu": "Gujarati", "ht": "Haitian Creole", "ha": "Hausa", "haw": "Hawaiian", "iw": "Hebrew", "hi": "Hindi", "hmn": "Hmong", "hu": "Hungarian", "is": "Icelandic", "ig": "Igbo", "id": "Indonesian", "ga": "Irish", "it": "Italian", "ja": "Japanese", "jv": "Javanese", "kn": "Kannada", "kk": "Kazakh", "km": "Khmer", "rw": "Kinyarwanda", "ko": "Korean", "kri": "Krio", "ku": "Kurdish", "ky": "Kyrgyz", "lo": "Lao", "la": "Latin", "lv": "Latvian", "ln": "Lingala", "lt": "Lithuanian", "lb": "Luxembourgish", "mk": "Macedonian", "mg": "Malagasy", "ms": "Malay", "ml": "Malayalam", "mt": "Maltese", "mi": "Māori", "mr": "Marathi", "mn": "Mongolian", "ne": "Nepali", "nso": "Northern Sotho", "no": "Norwegian", "ny": "Nyanja", "or": "Odia", "om": "Oromo", "ps": "Pashto", "fa": "Persian", "pl": "Polish", "pt": "Portuguese", "pa": "Punjabi", "qu": "Quechua", "ro": "Romanian", "ru": "Russian", "sm": "Samoan", "sa": "Sanskrit", "gd": "Scottish Gaelic", "sr": "Serbian", "sn": "Shona", "sd": "Sindhi", "si": "Sinhala", "sk": "Slovak", "sl": "Slovenian", "so": "Somali", "st": "Southern Sotho", "es": "Spanish", "su": "Sundanese", "sw": "Swahili", "sv": "Swedish", "tg": "Tajik", "ta": "Tamil", "tt": "Tatar", "te": "Telugu", "th": "Thai", "ti": "Tigrinya", "ts": "Tsonga", "tr": "Turkish", "tk": "Turkmen", "uk": "Ukrainian", "ur": "Urdu", "ug": "Uyghur", "uz": "Uzbek", "vi": "Vietnamese", "cy": "Welsh", "fy": "Western Frisian", "xh": "Xhosa", "yi": "Yiddish", "yo": "Yoruba", "zu": "Zulu" }, streamNoConnectionToServer: "There is no connection to the server", searchField: "Search...", VOTTranslateAPIErrors: "Translate errors from the API", VOTDetectService: "Language detection service", VOTProxyWorkerHost: "Enter the proxy worker address", VOTM3u8ProxyHost: "Enter the address of the m3u8 proxy worker", proxySettings: "Proxy Settings", translationTakeApproximatelyMinute2: "The translation will take approximately {0} minutes", VOTAudioBooster: "Extended translation volume increase", VOTSubtitlesDesign: "Subtitles design", VOTSubtitlesFont: "Subtitle font", VOTSubtitlesFontSize: "Font size of subtitles", VOTSubtitlesOpacity: "Transparency of the subtitle background", VOTSubtitlesDownloadFormat: "The format for downloading subtitles", VOTDownloadWithName: "Download files with the video name", VOTUpdateLocaleFiles: "Update localization files", VOTLocaleHash: "Locale hash", VOTUpdatedAt: "Updated at", VOTNeedWebAudioAPI: "To enable this, you must have a Web Audio API", VOTMediaCSPEnabledOnSite: "Media CSP is enabled on this site", VOTOnlyBypassMediaCSP: "Use it only for bypassing Media CSP", VOTNewAudioPlayer: "Use the new audio player", VOTUseNewModel: "Use an experimental variation of Yandex voices for some videos", TranslationDelayed: "The translation is slightly delayed", VOTTranslationCompletedNotify: "The translation on the {0} has been completed!", VOTSendNotifyOnComplete: "Send a notification that the video has been translated", VOTBugReport: "Report a bug", VOTTranslateProxyDisabled: "Disabled", VOTTranslateProxyEnabled: "Enabled", VOTTranslateProxyEverything: "Proxy everything", VOTTranslateProxyStatus: "Proxying mode", VOTTranslatedBy: "Translated by {0}", VOTStreamNotAvailable: "Translate stream isn't available", VOTTranslationTextService: "Text translation service", VOTNotAffectToVoice: "Doesn't affect the translation of text in voice over", DontTranslateSelectedLanguages: "Don't translate from selected languages", showVideoVolumeSlider: "Display the video volume slider", hotkeysSettings: "Hotkeys settings", None: "None", VOTStandardVoicesTitle: "Standard voices", VOTStandardVoicesSubtitle: "Fast and high-quality voiceover", VOTLiveVoicesTitle: "Live voices", VOTLiveVoicesSubtitle: "Maximum similarity. As if everyone knows Russian", miscSettings: "Misc settings", services: { "yandexbrowser": "Yandex Browser", "msedge": "Microsoft Edge", "rust-server": "Rust Server" }, aboutExtension: "About extension", appearance: "Appearance", buttonPosition: "Button position in the player", position: { "left": "Left", "right": "Right", "top": "Top", "default": "Default", "leftCenter": "Left centered", "rightCenter": "Right centered" }, secs: "secs", autoHideButtonDelay: "Delay before hiding the translate button", notFound: "not found", minButtonPositionContainer: "The button position only changes in players larger than 600 pixels.", VOTTranslateProxyStatusDefault: "Completely disabling proxying in your country may break the extension", PressTheKeyCombination: "Press the key combination...", VOTUseAudioDownload: "Use audio download", VOTUseAudioDownloadWarning: "Disabling audio downloads may affect the functionality of the extension", VOTAccountRequired: "You need to log in to use this feature", VOTMyAccount: "My account", VOTLogin: "Login", VOTLogout: "Logout", VOTRefresh: "Refresh", VOTYandexToken: "Enter the Yandex OAuth Token", VOTYandexTokenInfo: "You can manually set the account token in this field. Please note that we don't check its validity before sending a translate request", VOTLoginViaToken: "Login via token", smartDucking: "Adaptive volume", VOTYandexTokenExpired: "Session expired. Log in again", VOTVoiceSelection: "Choose dubbing", VOTRetryTranslation: "Retry translation" }; //#endregion //#region src/localization/localizationProvider.ts var LOCALE_STORAGE_KEYS = [ "localePhrases", "localeLang", "localeHash", "localeVersion", "localeUpdatedAt", "localeLangOverride" ]; var DEFAULT_LOCALE = toFlatObj(en_default); var repoBranch = "master"; var availableLocales = (() => { const locales = Array.isArray([ "auto", "en", "ru", "af", "am", "ar", "az", "bg", "bn", "bs", "ca", "cs", "cy", "da", "de", "el", "es", "et", "eu", "fa", "fi", "fr", "gl", "hi", "hr", "hu", "hy", "id", "it", "ja", "jv", "kk", "km", "kn", "ko", "lo", "mk", "ml", "mn", "ms", "mt", "my", "ne", "nl", "pa", "pl", "pt", "ro", "si", "sk", "sl", "sq", "sr", "su", "sv", "sw", "tr", "uk", "ur", "uz", "vi", "zh", "zu" ]) ? [ "auto", "en", "ru", "af", "am", "ar", "az", "bg", "bn", "bs", "ca", "cs", "cy", "da", "de", "el", "es", "et", "eu", "fa", "fi", "fr", "gl", "hi", "hr", "hu", "hy", "id", "it", "ja", "jv", "kk", "km", "kn", "ko", "lo", "mk", "ml", "mn", "ms", "mt", "my", "ne", "nl", "pa", "pl", "pt", "ro", "si", "sk", "sl", "sq", "sr", "su", "sv", "sw", "tr", "uk", "ur", "uz", "vi", "zh", "zu" ] : ["en"]; return locales.includes("auto") ? locales : ["auto", ...locales]; })(); function resolveRuntimeLocaleVersion(buildVersion, scriptVersion) { return buildVersion || scriptVersion || "unknown"; } function getRuntimeLocaleVersion() { return resolveRuntimeLocaleVersion(String("1.11.17"), typeof GM_info === "undefined" ? "" : String(GM_info?.script?.version || "")); } var LocalizationProvider = class { /** * Language used before page was reloaded */ lang; /** * Locale phrases with current language */ locale; defaultLocale = DEFAULT_LOCALE; localesUrl = `${contentUrl}/${repoBranch}/src/localization/locales`; hashesUrl = `${contentUrl}/${repoBranch}/src/localization/hashes.json`; warnedMissingKeys = /* @__PURE__ */ new Set(); _langOverride = "auto"; constructor() { this.lang = this.getLang(); this.locale = {}; } async init() { const [langOverride, phrases] = await Promise.all([votStorage.get("localeLangOverride", "auto"), votStorage.get("localePhrases", "")]); this._langOverride = langOverride; this.lang = this.getLang(); this.setLocaleFromJsonString(phrases); return this; } get langOverride() { return this._langOverride; } getLang() { return this.langOverride === "auto" ? lang : this.langOverride; } getAvailableLangs() { return [...availableLocales]; } async reset() { await Promise.all(LOCALE_STORAGE_KEYS.map((key) => votStorage.delete(key))); return this; } buildUrl(baseUrl, path = "", force = false) { return `${baseUrl}${path}${force ? `?timestamp=${getTimestamp()}` : ""}`; } async changeLang(newLang) { if (this.langOverride === newLang) return false; await votStorage.set("localeLangOverride", newLang); this._langOverride = newLang; this.lang = this.getLang(); await this.update(true); return true; } async checkUpdates(force = false) { debug.log("Check locale updates..."); try { const runtimeLocaleVersion = getRuntimeLocaleVersion(); if (!force) { const storedLocaleVersion = await votStorage.get("localeVersion", ""); if (runtimeLocaleVersion !== "unknown" && storedLocaleVersion === runtimeLocaleVersion) return false; } const res = await GM_fetch(this.buildUrl(this.hashesUrl, "", force)); if (!res.ok) throw res.status; const hashes = await res.json(); if (!hashes || typeof hashes !== "object") throw new Error("Invalid locale hashes payload"); const nextHash = hashes[this.lang]; if (typeof nextHash !== "string" || !nextHash) return false; return await votStorage.get("localeHash", "") === nextHash ? false : nextHash; } catch (err) { console.error("[VOT] [localizationProvider] Failed to get locales hash:", err); return null; } } async update(force = false) { const runtimeLocaleVersion = getRuntimeLocaleVersion(); const storedLocaleVersion = await votStorage.get("localeVersion", ""); const hash = await this.checkUpdates(force); if (hash === null) return this; if (!hash) { if (storedLocaleVersion !== runtimeLocaleVersion) await votStorage.set("localeVersion", runtimeLocaleVersion); return this; } const timestamp = getTimestamp(); debug.log("Updating locale..."); try { const res = await GM_fetch(this.buildUrl(this.localesUrl, `/${this.lang}.json`, force)); if (!res.ok) throw res.status; const text = await res.text(); this.setLocaleFromJsonString(text); await Promise.all([ votStorage.set("localePhrases", text), votStorage.set("localeHash", hash), votStorage.set("localeLang", this.lang), votStorage.set("localeVersion", runtimeLocaleVersion), votStorage.set("localeUpdatedAt", timestamp) ]); } catch (err) { console.error("[VOT] [localizationProvider] Failed to get locale:", err); this.setLocaleFromJsonString(await votStorage.get("localePhrases", "")); } return this; } setLocaleFromJsonString(json) { const trimmed = json.trim(); if (!trimmed) { this.locale = {}; this.warnedMissingKeys.clear(); return this; } try { const locale = JSON.parse(trimmed); if (!locale || typeof locale !== "object" || Array.isArray(locale)) throw new Error("Locale payload should be a JSON object"); this.locale = toFlatObj(locale); } catch (err) { console.error("[VOT] [localizationProvider]", err); this.locale = {}; } this.warnedMissingKeys.clear(); return this; } getFromLocale(locale, key, source = "locale") { return locale[key] ?? this.warnMissingKey(locale, key, source); } warnMissingKey(locale, key, source) { const warningKey = `${source}:${key}`; if (this.warnedMissingKeys.has(warningKey)) return; this.warnedMissingKeys.add(warningKey); console.warn("[VOT] [localizationProvider] locale", locale, "doesn't contain key", key); } getDefault(key) { return this.getFromLocale(this.defaultLocale, key, "default") ?? key; } get(key) { return this.getFromLocale(this.locale, key) ?? this.getDefault(key); } getLangLabel(lang) { const key = `langs.${lang}`; if (key in this.defaultLocale) { const label = this.get(key); if (label) return label; } return lang.toUpperCase(); } }; var localizationProvider = new LocalizationProvider(); /** * In the userscript build, SystemJS wrapping allowed a top-level await. * For the extension build we bootstrap through loader scripts and keep the * runtime initialization explicit, so avoid top-level await and expose a lazy * ready Promise instead. */ var localizationProviderReadyPromise = null; function ensureLocalizationProviderReady() { localizationProviderReadyPromise ??= localizationProvider.init(); return localizationProviderReadyPromise; } //#endregion //#region src/utils/iframeConnector.ts /** * Runtime frame detection helper. * * Audio download no longer relies on service iframes or postMessage bridges. * We keep only the minimal utility used by bootstrap policy. */ var isIframe = () => globalThis.self !== globalThis.top; //#endregion //#region src/bootstrap/runtimeActivation.ts var runtimeActivated = false; var runtimeActivationPromise = null; async function activateRuntime(reason, logBootstrap) { logBootstrap("Activating runtime", { reason }); if (globalThis.location.origin === "https://rust-server-531j.onrender.com") { await initAuth(); runtimeActivated = true; return; } await ensureLocalizationProviderReady(); if (!isIframe()) await localizationProvider.update(); debug.log(`Selected menu language: ${localizationProvider.lang}`); runtimeActivated = true; } async function ensureRuntimeActivated(reason, logBootstrap) { if (runtimeActivated) return; runtimeActivationPromise ??= activateRuntime(reason, logBootstrap).finally(() => { runtimeActivationPromise = null; }); await runtimeActivationPromise; } //#endregion //#region src/bootstrap/videoObserverBinding.ts var boundObservers = /* @__PURE__ */ new WeakSet(); var RUNTIME_URL_HOSTS = /* @__PURE__ */ new Set(["peertube", "directlink"]); function bindObserverListeners(options) { const { videoObserver, videosWrappers, ensureRuntimeActivated, getServicesCached, findContainer, createVideoHandler, resolveVideoId = (site, video) => getVideoID(site, { fetchFn: GM_fetch, video }) } = options; if (boundObservers.has(videoObserver)) return; boundObservers.add(videoObserver); const initializingVideos = /* @__PURE__ */ new WeakSet(); const containerOwners = /* @__PURE__ */ new WeakMap(); const videoContainers = /* @__PURE__ */ new WeakMap(); const pendingVideoByContainer = /* @__PURE__ */ new WeakMap(); const clearContainerOwner = (video) => { const container = videoContainers.get(video); if (container && containerOwners.get(container) === video) containerOwners.delete(container); videoContainers.delete(video); return container ?? void 0; }; const releaseVideoHandler = async (video, reason) => { const videoHandler = videosWrappers.get(video); if (!videoHandler) return; try { await videoHandler.release(); } catch (error) { console.error(`[VOT] Failed to release videoHandler (${reason})`, error); } finally { if (videosWrappers.get(video) === videoHandler) videosWrappers.delete(video); } }; const getMatchedSiteAndContainer = (video) => { for (const candidate of getServicesCached()) { const container = findContainer(candidate, video); if (container) return { site: candidate, container }; } return null; }; const withRuntimeSiteUrl = (site) => { return RUNTIME_URL_HOSTS.has(String(site.host)) ? { ...site, url: globalThis.location.origin } : site; }; const tryReplaceVideo = async (oldVideo, newVideo, container) => { const videoHandler = videosWrappers.get(oldVideo); const previousVideoId = videoHandler?.videoData?.videoId; if (!videoHandler?.hasActiveSource() || !previousVideoId) return false; try { if (await resolveVideoId(videoHandler.site, newVideo) !== previousVideoId) return false; await videoHandler.replaceVideo(newVideo); } catch (error) { console.error("[VOT] Failed to replace video element", error); return false; } if (videosWrappers.get(oldVideo) !== videoHandler) return videosWrappers.get(newVideo) === videoHandler; videosWrappers.delete(oldVideo); videoContainers.delete(oldVideo); videosWrappers.set(newVideo, videoHandler); videoContainers.set(newVideo, container); containerOwners.set(container, newVideo); return true; }; const promotePendingVideo = async (container) => { const pendingVideo = container && pendingVideoByContainer.get(container); if (!pendingVideo) return; pendingVideoByContainer.delete(container); if (!pendingVideo.isConnected || videosWrappers.has(pendingVideo) || initializingVideos.has(pendingVideo)) return; await handleVideoAdded(pendingVideo); }; const handleVideoAdded = async (video) => { if (videosWrappers.has(video) || initializingVideos.has(video)) return; initializingVideos.add(video); try { if (!await ensureRuntimeReady()) return; const match = getMatchedSiteAndContainer(video); if (!match) return; const { site, container } = match; const activeVideoForContainer = containerOwners.get(container); if (activeVideoForContainer && activeVideoForContainer !== video) { if (activeVideoForContainer.isConnected) { pendingVideoByContainer.set(container, video); return; } if (await tryReplaceVideo(activeVideoForContainer, video, container)) return; await releaseVideoHandler(activeVideoForContainer, "stale container"); clearContainerOwner(activeVideoForContainer); } const videoHandler = createVideoHandler(video, container, withRuntimeSiteUrl(site)); videosWrappers.set(video, videoHandler); videoContainers.set(video, container); containerOwners.set(container, video); try { await videoHandler.init(); if (videosWrappers.get(video) !== videoHandler) return; try { await videoHandler.setCanPlay(); } catch (err) { console.error("[VOT] Failed to get video data", err); } } catch (err) { if (videosWrappers.get(video) === videoHandler) { await releaseVideoHandler(video, "init failed"); const container = clearContainerOwner(video); if (container) pendingVideoByContainer.delete(container); await promotePendingVideo(container); } console.error("[VOT] Failed to initialize videoHandler", err); } } finally { initializingVideos.delete(video); } }; const ensureRuntimeReady = async () => { try { await ensureRuntimeActivated("video-detected"); return true; } catch (err) { console.error("[VOT] Failed to activate runtime", err); return false; } }; videoObserver.onVideoAdded.addListener(handleVideoAdded); videoObserver.onVideoRemoved.addListener(async (video) => { const container = videoContainers.get(video); const replacement = container && Array.from(container.querySelectorAll("video")).find((candidate) => candidate !== video && candidate.isConnected); if (container && replacement && await tryReplaceVideo(video, replacement, container)) { initializingVideos.delete(video); return; } clearContainerOwner(video); await releaseVideoHandler(video, "video removed"); initializingVideos.delete(video); if (container && pendingVideoByContainer.get(container) === video) pendingVideoByContainer.delete(container); await promotePendingVideo(container); }); } //#endregion //#region src/core/bootstrapPolicy.ts function shouldSkipIframeBootstrap(input) { if (!input.isIframe) return false; return input.href === "about:blank" || input.href.startsWith("about:srcdoc") || input.origin === "https://www.youtube.com" && input.href.includes("#ya_iframe") || input.origin === "null"; } function resolveBootstrapMode(input) { if (shouldSkipIframeBootstrap(input)) return "skip"; if (!input.isIframe && input.origin === input.authOrigin) return "auth-eager"; return "lazy"; } //#endregion //#region src/utils/dom.ts function getComposableParent(node) { if (!node) return null; if (typeof ShadowRoot !== "undefined" && node instanceof ShadowRoot) return node.host; return node.parentNode ?? null; } function getDeepActiveElement(root = document) { let activeElement = root.activeElement; while (activeElement instanceof HTMLElement && activeElement.shadowRoot) { const nestedActiveElement = activeElement.shadowRoot.activeElement; if (!nestedActiveElement) break; activeElement = nestedActiveElement; } return activeElement; } /** * Checks whether `target` is a descendant of `container` in the composed tree * (crossing ShadowRoot boundaries via hosts). */ function containsCrossShadow(container, target) { let node = target; while (node) { if (node === container) return true; node = getComposableParent(node); } return false; } function closestCrossShadow(element, selector) { if (!element || !selector) return null; return walkCrossShadow(element, selector, element instanceof Document ? null : element); } function findMatchingDocumentElement(current, selector, origin) { if (!origin) return current.querySelector(selector); const matches = current.querySelectorAll(selector); for (const match of matches) if (containsCrossShadow(match, origin)) return match; return null; } function getNextCrossShadowTarget(current) { const root = current.getRootNode(); if (root instanceof ShadowRoot) return root.host; if (root instanceof Document) return root; if (root !== current) { const parent = getComposableParent(root); if (parent && parent !== current && parent instanceof Element) return parent; } return null; } function walkCrossShadow(current, selector, origin) { if (!current) return null; if (current instanceof Document) return findMatchingDocumentElement(current, selector, origin); const closest = current.closest(selector); if (closest) return closest; return walkCrossShadow(getNextCrossShadowTarget(current), selector, origin); } //#endregion //#region src/core/containerResolution.ts function findConnectedContainerBySelector(video, selector) { if (!selector) return null; const matched = closestCrossShadow(video, selector); if (matched instanceof HTMLElement && matched.isConnected && containsCrossShadow(matched, video)) return matched; return null; } //#endregion //#region src/utils/environment.ts var UNKNOWN_VALUE = "unknown"; var joinParts = (...parts) => { return parts.filter(Boolean).join(" ").trim() || UNKNOWN_VALUE; }; function isDocumentHidden() { return typeof document !== "undefined" && document.hidden; } function getEnvironmentInfo() { return { os: joinParts(browserInfo.os?.name, browserInfo.os?.version), browser: joinParts(browserInfo.browser?.name, browserInfo.browser?.version), loader: (() => { const handler = GM_info?.scriptHandler; const version = GM_info?.version; if (handler && version) return `${handler} v${version}`; return handler || version || UNKNOWN_VALUE; })(), scriptVersion: GM_info?.script?.version ?? UNKNOWN_VALUE, scriptName: GM_info?.script?.name ?? UNKNOWN_VALUE, url: globalThis?.location?.href ?? UNKNOWN_VALUE }; } //#endregion //#region src/utils/intervalIdleChecker.ts var DEFAULT_PROFILE = { checkIntervalMs: 250, idleAfterMs: 180 }; function normalizePositiveMs(value, fallback) { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.max(1, Math.trunc(value)); } function normalizeNonNegativeMs(value, fallback) { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.max(0, Math.trunc(value)); } function normalizeProfile(profile = {}) { return { checkIntervalMs: normalizePositiveMs(profile.checkIntervalMs, DEFAULT_PROFILE.checkIntervalMs), idleAfterMs: normalizeNonNegativeMs(profile.idleAfterMs, DEFAULT_PROFILE.idleAfterMs) }; } function getDefaultRuntime() { return { nowMs: () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(), setInterval: globalThis.setInterval.bind(globalThis), clearInterval: globalThis.clearInterval.bind(globalThis), queueMicrotask: (fn) => { globalThis.queueMicrotask(fn); }, onVisibilityChange: (listener) => { if (typeof document === "undefined" || typeof document.addEventListener !== "function") return () => void 0; document.addEventListener("visibilitychange", listener); return () => { if (typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", listener); }; } }; } var IntervalIdleChecker = class { profile; runtime; subscribers = /* @__PURE__ */ new Set(); intervalId = null; unsubscribeVisibilityChange = null; running = false; destroyed = false; immediateQueued = false; currentMode = "active"; lastActivityAt; onVisibilityChangeHandler = () => { if (this.destroyed || !this.running) return; if (isDocumentHidden()) this.clearIntervalTimer(); else this.armInterval(); this.requestImmediateTick(); }; constructor(options = {}) { this.profile = normalizeProfile(options.profile); this.runtime = { ...getDefaultRuntime(), ...options.runtime }; this.lastActivityAt = this.runtime.nowMs(); } start() { if (this.destroyed || this.running) return; this.running = true; this.lastActivityAt = this.runtime.nowMs(); this.subscribeVisibilityChange(); this.armInterval(); this.runTick("start"); } stop() { if (!this.running) return; this.running = false; this.clearIntervalTimer(); this.immediateQueued = false; this.unsubscribeFromVisibilityChange(); } destroy() { if (this.destroyed) return; this.stop(); this.subscribers.clear(); this.destroyed = true; } subscribe(fn) { if (this.destroyed) return () => void 0; this.subscribers.add(fn); return () => { this.subscribers.delete(fn); }; } markActivity(_source) { if (this.destroyed) return; this.lastActivityAt = this.runtime.nowMs(); if (!this.running) return; const nextMode = this.resolveMode(this.lastActivityAt); if (nextMode !== this.currentMode) this.currentMode = nextMode; } requestImmediateTick() { if (this.destroyed || !this.running || this.immediateQueued) return; this.immediateQueued = true; this.runtime.queueMicrotask(() => { this.immediateQueued = false; if (this.destroyed || !this.running) return; this.runTick("immediate"); }); } resolveMode(nowMs) { if (isDocumentHidden()) return "hidden"; return nowMs - this.lastActivityAt >= this.profile.idleAfterMs ? "idle" : "active"; } clearIntervalTimer() { if (this.intervalId === null) return; this.runtime.clearInterval(this.intervalId); this.intervalId = null; } armInterval() { if (this.intervalId !== null) return; this.intervalId = this.runtime.setInterval(() => { this.runTick("interval"); }, this.profile.checkIntervalMs); } runTick(source) { if (this.destroyed || !this.running) return; if (this.subscribers.size === 0) return; const nowMs = this.runtime.nowMs(); const nextMode = this.resolveMode(nowMs); if (nextMode !== this.currentMode) this.currentMode = nextMode; const ctx = { nowMs, mode: nextMode, source }; for (const sub of this.subscribers) try { sub(ctx); } catch {} } subscribeVisibilityChange() { if (this.unsubscribeVisibilityChange !== null) return; this.unsubscribeVisibilityChange = this.runtime.onVisibilityChange(this.onVisibilityChangeHandler); } unsubscribeFromVisibilityChange() { if (this.unsubscribeVisibilityChange === null) return; this.unsubscribeVisibilityChange(); this.unsubscribeVisibilityChange = null; } }; function createIntervalIdleChecker(profile) { return new IntervalIdleChecker({ profile }); } //#endregion //#region src/utils/domTraversal.ts function getComposedParentElement(node) { if (!node) return null; const parentElement = node.parentElement ?? null; if (parentElement) return parentElement; if (typeof node.getRootNode !== "function") return null; const root = node.getRootNode(); if (root && "host" in root) return root.host ?? null; return null; } function someComposedAncestor(node, predicate) { for (let parent = getComposedParentElement(node); parent; parent = getComposedParentElement(parent)) if (predicate(parent)) return true; return false; } function isArrayLikeChildren(children) { return "length" in children; } function pushChildrenToStack(stack, stackSize, children) { if (isArrayLikeChildren(children)) { const arrayLike = children; for (let index = 0; index < arrayLike.length; index += 1) { const child = arrayLike[index]; if (child !== void 0 && child !== null) { stack[stackSize] = child; stackSize += 1; } } } else for (const child of children) if (child !== void 0 && child !== null) { stack[stackSize] = child; stackSize += 1; } return stackSize; } function walkShadowIncludingSubtree(root, adapter, visit) { const stack = [root]; const { getChildren, getShadowRoot } = adapter; let stackSize = 1; while (stackSize > 0) { const node = stack[stackSize - 1]; stackSize -= 1; visit(node); stackSize = pushChildrenToStack(stack, stackSize, getChildren(node)); const shadowRoot = getShadowRoot(node); if (shadowRoot) { stack[stackSize] = shadowRoot; stackSize += 1; } } } //#endregion //#region src/utils/eventImpl.ts var EventImpl = class { listeners = /* @__PURE__ */ new Set(); get size() { return this.listeners.size; } addListener(handler) { this.listeners.add(handler); return this; } removeListener(handler) { this.listeners.delete(handler); return this; } dispatch(...args) { for (const handler of this.listeners) try { handler(...args); } catch (exception) { console.error("[VOT]", exception); } } async dispatchAsync(...args) { const pending = []; for (const handler of this.listeners) try { const result = handler(...args); if (result && typeof result.then === "function") pending.push(Promise.resolve(result)); } catch (exception) { console.error("[VOT]", exception); } if (!pending.length) return; const settled = await Promise.allSettled(pending); for (const item of settled) if (item.status === "rejected") console.error("[VOT]", item.reason); } clear() { this.listeners.clear(); } }; //#endregion //#region src/utils/VideoObserver.ts var AD_ATTRS = [ "class", "id", "title" ]; var AD_KEYWORD_PATTERN = new RegExp([ "advertise", "advertisement", "promo", "sponsor", "banner", "commercial", "preroll", "midroll", "postroll", "ad-container", "sponsored" ].map((keyword) => keyword.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)).join("|")); var ATTACH_SHADOW_HOOK_KEY = Symbol.for("vot.attachShadowHook"); function getAttachShadowDescriptor() { const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, "attachShadow"); if (!descriptor || typeof descriptor.value !== "function") return null; return descriptor; } function getOrInstallAttachShadowHook() { const g = globalThis; const existing = g[ATTACH_SHADOW_HOOK_KEY]; if (existing?.descriptor && existing.subscribers instanceof Set) return existing; const descriptor = getAttachShadowDescriptor(); if (!descriptor) return null; const original = descriptor.value; const state = { descriptor, subscribers: /* @__PURE__ */ new Set() }; const patchedAttachShadow = function(init) { const root = original.call(this, init); for (const sub of state.subscribers) try { sub(root); } catch (error) { debug.error("attachShadow subscriber failed", error); } return root; }; try { Object.defineProperty(Element.prototype, "attachShadow", { ...descriptor, value: patchedAttachShadow }); } catch { return null; } g[ATTACH_SHADOW_HOOK_KEY] = state; return state; } function removeAttachShadowSubscriber(subscriber) { const g = globalThis; const state = g[ATTACH_SHADOW_HOOK_KEY]; if (!state) return; state.subscribers.delete(subscriber); if (state.subscribers.size > 0) return; try { Object.defineProperty(Element.prototype, "attachShadow", state.descriptor); } catch { const original = state.descriptor.value; if (typeof original === "function") Element.prototype.attachShadow = original; } delete g[ATTACH_SHADOW_HOOK_KEY]; } var VideoObserver = class VideoObserver { seenVideos = /* @__PURE__ */ new WeakSet(); activeVideos = /* @__PURE__ */ new WeakSet(); observedRoots = /* @__PURE__ */ new WeakSet(); videoListenerControllers = /* @__PURE__ */ new Map(); pendingAdded = /* @__PURE__ */ new Set(); pendingRemoved = /* @__PURE__ */ new Set(); flushPending = false; static MAX_FLUSH_BUDGET_MS = 6; static MAX_NODES_PER_SLICE = 120; onVideoAdded = new EventImpl(); onVideoRemoved = new EventImpl(); observer = new MutationObserver((muts) => this.onMutations(muts)); intervalIdleChecker; checkerUnsubscribe = null; enabled = false; attachShadowSubscriber = null; onDocumentReady = null; onPageShow = () => { const root = document.documentElement; if (!root) return; this.pendingAdded.add(root); this.scheduleFlush(); }; constructor(intervalIdleChecker = createIntervalIdleChecker()) { this.intervalIdleChecker = intervalIdleChecker; } static containsAdKeyword(value) { return value.length > 0 && AD_KEYWORD_PATTERN.test(value); } isAdRelated(element) { for (const attr of AD_ATTRS) { const rawValue = element.getAttribute(attr); if (!rawValue) continue; if (VideoObserver.containsAdKeyword(rawValue.toLowerCase())) return true; } return false; } isInsideAd(video) { return someComposedAncestor(video, (p) => this.isAdRelated(p)); } getCapturedAudioTrackCount(video) { const candidate = video; const captureStream = candidate.captureStream ?? candidate.mozCaptureStream; if (typeof captureStream !== "function") return null; try { return captureStream.call(video).getAudioTracks().length; } catch { return null; } } isLikelySilentDecorativeVideo(video) { if (!(video.muted || video.defaultMuted)) return false; if (!video.autoplay || !video.loop) return false; if (video.controls) return false; const v = video; if (typeof v.mozHasAudio === "boolean") return !v.mozHasAudio; if ("audioTracks" in v && typeof v.audioTracks?.length === "number") { if (v.audioTracks.length > 0) return false; const capturedTrackCount = this.getCapturedAudioTrackCount(video); if (capturedTrackCount !== null) return capturedTrackCount === 0; return true; } const capturedTrackCount = this.getCapturedAudioTrackCount(video); if (capturedTrackCount !== null) return capturedTrackCount === 0; return false; } hasAudio(video) { const v = video; if (video.srcObject instanceof MediaStream) return video.srcObject.getAudioTracks().length > 0; if (typeof v.mozHasAudio === "boolean") return v.mozHasAudio; if (typeof v.webkitAudioDecodedByteCount === "number" && v.webkitAudioDecodedByteCount > 0) return true; if ("audioTracks" in v && typeof v.audioTracks?.length === "number") { if (v.audioTracks.length > 0) return true; } if (this.isLikelySilentDecorativeVideo(video)) return false; return true; } isValidVideo(video) { if (this.isAdRelated(video)) return false; if (this.isInsideAd(video)) return false; if (!this.hasAudio(video)) { debug.log("Ignoring video without audio:", video); return false; } return true; } observeRoot(root) { if (this.observedRoots.has(root)) return; this.observedRoots.add(root); this.observer.observe(root, { childList: true, subtree: true }); } static domAdapter = { getChildren: (node) => Array.from(node.children ?? []), getShadowRoot: (node) => node.shadowRoot }; scan(root) { if (root instanceof HTMLVideoElement) { this.trackVideo(root); return; } if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_FRAGMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return; walkShadowIncludingSubtree(root, VideoObserver.domAdapter, (el) => { if (el instanceof HTMLVideoElement) { this.trackVideo(el); return; } const sr = el.shadowRoot; if (sr) this.observeRoot(sr); }); } getVideoListenerSignal(video) { const existingController = this.videoListenerControllers.get(video); if (existingController) existingController.abort(); const controller = new AbortController(); this.videoListenerControllers.set(video, controller); return controller.signal; } cleanupVideoListeners(video) { const controller = this.videoListenerControllers.get(video); if (!controller) return; controller.abort(); this.videoListenerControllers.delete(video); } cleanupAllVideoListeners() { for (const controller of this.videoListenerControllers.values()) controller.abort(); this.videoListenerControllers.clear(); } trackVideo(video) { if (this.seenVideos.has(video)) return; this.seenVideos.add(video); const listenerSignal = this.getVideoListenerSignal(video); const tryValidate = () => { if (this.isValidVideo(video)) { if (!this.activeVideos.has(video)) { this.activeVideos.add(video); this.onVideoAdded.dispatch(video); } } }; if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) tryValidate(); else { video.addEventListener("loadeddata", tryValidate, { once: true, signal: listenerSignal }); const handlePlay = () => { if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) tryValidate(); }; video.addEventListener("play", handlePlay, { once: true, passive: true, signal: listenerSignal }); } video.addEventListener("emptied", () => { if (!video.isConnected) this.untrackVideo(video); }, { passive: true, signal: listenerSignal }); } untrackVideo(video) { this.cleanupVideoListeners(video); if (this.activeVideos.has(video)) { this.onVideoRemoved.dispatch(video); this.activeVideos.delete(video); } this.seenVideos.delete(video); } collectVideos(node) { const set = /* @__PURE__ */ new Set(); if (node instanceof HTMLVideoElement) set.add(node); if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE && node.nodeType !== Node.DOCUMENT_NODE) return Array.from(set); walkShadowIncludingSubtree(node, VideoObserver.domAdapter, (el) => { if (el instanceof HTMLVideoElement) set.add(el); }); return Array.from(set); } getNowMs() { if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now(); return Date.now(); } isSliceBudgetReached(startMs, processed) { if (processed >= VideoObserver.MAX_NODES_PER_SLICE) return true; return this.getNowMs() - startMs >= VideoObserver.MAX_FLUSH_BUDGET_MS; } processPendingAdded(startMs) { let processed = 0; while (this.pendingAdded.size > 0) { const next = this.pendingAdded.values().next(); if (next.done) break; this.pendingAdded.delete(next.value); this.scan(next.value); processed += 1; if (this.isSliceBudgetReached(startMs, processed)) break; } return processed; } processPendingRemoved(startMs, processed) { let processedCount = processed; while (this.pendingRemoved.size > 0) { if (this.isSliceBudgetReached(startMs, processedCount)) break; const next = this.pendingRemoved.values().next(); if (next.done) break; this.pendingRemoved.delete(next.value); for (const video of this.collectVideos(next.value)) if (!video.isConnected) this.untrackVideo(video); processedCount += 1; } return processedCount; } flushSlice = () => { if (!this.enabled) { this.pendingAdded.clear(); this.pendingRemoved.clear(); this.flushPending = false; return; } const startMs = this.getNowMs(); const processedAdded = this.processPendingAdded(startMs); this.processPendingRemoved(startMs, processedAdded); this.flushPending = this.pendingAdded.size > 0 || this.pendingRemoved.size > 0; if (this.flushPending) this.intervalIdleChecker.requestImmediateTick(); }; onCheckerTick = () => { if (!this.flushPending) return; this.flushSlice(); }; scheduleFlush = () => { if (!this.enabled) return; this.flushPending = true; this.intervalIdleChecker.requestImmediateTick(); }; installAttachShadowHook() { if (this.attachShadowSubscriber) return; const state = getOrInstallAttachShadowHook(); if (!state) return; const subscriber = (root) => { if (!this.enabled) return; this.observeRoot(root); this.pendingAdded.add(root); this.scheduleFlush(); }; state.subscribers.add(subscriber); this.attachShadowSubscriber = subscriber; } uninstallAttachShadowHook() { if (!this.attachShadowSubscriber) return; removeAttachShadowSubscriber(this.attachShadowSubscriber); this.attachShadowSubscriber = null; } enqueueAddedNode(node) { if (node.nodeType === Node.ELEMENT_NODE) { const shadowRoot = node.shadowRoot; if (shadowRoot) this.observeRoot(shadowRoot); } this.pendingAdded.add(node); } enqueueMutation(mutation) { for (const node of mutation.addedNodes) this.enqueueAddedNode(node); for (const node of mutation.removedNodes) this.pendingRemoved.add(node); } onMutations(mutations) { for (const mutation of mutations) { if (mutation.type !== "childList") continue; this.enqueueMutation(mutation); } if (this.pendingAdded.size > 0 || this.pendingRemoved.size > 0) this.scheduleFlush(); } enable() { if (this.enabled) return; this.enabled = true; this.checkerUnsubscribe?.(); this.checkerUnsubscribe = this.intervalIdleChecker.subscribe(this.onCheckerTick); this.intervalIdleChecker.start(); this.intervalIdleChecker.markActivity("video-observer-enable"); this.installAttachShadowHook(); globalThis.addEventListener("pageshow", this.onPageShow, { passive: true }); const root = document.documentElement; if (root) { this.observeRoot(root); this.scan(root); return; } const onReady = () => { const r = document.documentElement; if (!r) return; document.removeEventListener("readystatechange", onReady); this.onDocumentReady = null; if (!this.enabled) return; this.observeRoot(r); this.scan(r); }; this.onDocumentReady = onReady; document.addEventListener("readystatechange", onReady); queueMicrotask(onReady); } disable() { if (!this.enabled) return; this.enabled = false; globalThis.removeEventListener("pageshow", this.onPageShow); if (this.onDocumentReady) { document.removeEventListener("readystatechange", this.onDocumentReady); this.onDocumentReady = null; } this.uninstallAttachShadowHook(); this.observer.disconnect(); this.cleanupAllVideoListeners(); this.flushPending = false; this.checkerUnsubscribe?.(); this.checkerUnsubscribe = null; this.intervalIdleChecker.stop(); this.pendingAdded.clear(); this.pendingRemoved.clear(); this.seenVideos = /* @__PURE__ */ new WeakSet(); this.activeVideos = /* @__PURE__ */ new WeakSet(); this.observedRoots = /* @__PURE__ */ new WeakSet(); } }; //#endregion //#region node_modules/@vot.js/core/dist/providers/base.js var BaseProvider = class { host; schema; /** * If you don't want to use the classic fetch * @includeExample examples/with_ofetch.ts[1:13] */ fetch; fetchOpts; userAgent = config_default$1.userAgent; requestLang; responseLang; /** * Headers for interacting with API */ headers = { "User-Agent": this.userAgent, "Accept-Language": "en", Pragma: "no-cache", "Cache-Control": "no-cache" }; hostSchemaRe = /(http(s)?):\/\//; constructor({ host = config_default$1.host, fetchFn = fetchWithTimeout, fetchOpts = {}, headers = {}, requestLang = "en", responseLang = "ru" } = {}) { const schema = this.hostSchemaRe.exec(host)?.[1]; this.host = schema ? host.replace(`${schema}://`, "") : host; this.schema = schema ?? "https"; this.fetch = fetchFn; this.fetchOpts = fetchOpts; this.headers = { ...this.headers, ...headers }; this.requestLang = requestLang; this.responseLang = responseLang; } /** * The standard method for requesting the Yandex API, if necessary, you can override how it is done in the example * @includeExample examples/with_axios.ts[4:41] */ async request(path, body, headers = {}, method = "POST", fetchOpts = {}) { const options = this.getOpts(new Blob([body]), headers, method, fetchOpts); try { const res = await this.fetch(`${this.schema}://${this.host}${path}`, options); const data = await res.arrayBuffer(); return { success: res.status === 200, data }; } catch (err) { return { success: false, data: err?.message }; } } async requestJSON(path, body = null, headers = {}, method = "POST", fetchOpts = {}) { const options = this.getOpts(body, { Accept: "application/json", "Content-Type": "application/json", ...headers }, method, fetchOpts); try { const res = await this.fetch(`${this.schema}://${this.host}${path}`, options); const data = await res.json(); return { success: res.status === 200, data }; } catch (err) { return { success: false, data: err?.message }; } } getOpts(body, headers = {}, method = "POST", fetchOpts = {}) { return { method, headers: { ...this.headers, ...headers }, body, ...this.fetchOpts, ...fetchOpts }; } }; //#endregion //#region node_modules/@vot.js/core/dist/client.js var VOTJSError = class extends Error { data; constructor(message, data = void 0) { super(message); this.data = data; this.name = "VOTJSError"; } }; var VOTClient$1 = class { provider; constructor({ provider, host, fetchFn, fetchOpts, requestLang = "en", responseLang = "ru", apiToken, headers } = {}) { const ProviderClass = provider ?? YandexProvider; this.provider = new ProviderClass({ host, fetchFn, fetchOpts, headers, apiToken, requestLang, responseLang }); } async translateVideo(opts) { return await this.provider.translateVideo(opts); } async translateStream(opts) { return await this.provider.translateStream(opts); } async getSubtitles(opts) { return await this.provider.getSubtitles(opts); } get requestLang() { return this.provider.requestLang; } set requestLang(lang) { this.provider.requestLang = lang; } get responseLang() { return this.provider.responseLang; } set responseLang(lang) { this.provider.responseLang = lang; } }; //#endregion //#region node_modules/@vot.js/core/dist/protobuf.js function encodeTranslationRequest(url, duration, requestLang, responseLang, translationHelp, { forceSourceLang = false, wasStream = false, videoTitle = "", bypassCache = false, useLivelyVoice = false, firstRequest = true } = {}) { return VideoTranslationRequest.encode({ url, firstRequest, duration, unknown0: true, language: requestLang, forceSourceLang, unknown1: false, translationHelp: translationHelp ?? [], responseLanguage: responseLang, wasStream, unknown2: true, configVersion: 2, bypassCache, useLivelyVoice, videoTitle }).finish(); } function decodeTranslationResponse(response) { return VideoTranslationResponse.decode(new Uint8Array(response)); } function encodeTranslationCacheRequest(url, duration, requestLang, responseLang) { return VideoTranslationCacheRequest.encode({ url, duration, language: requestLang, responseLanguage: responseLang }).finish(); } function decodeTranslationCacheResponse(response) { return VideoTranslationCacheResponse.decode(new Uint8Array(response)); } function isPartialAudioBuffer(audioBuffer) { return "chunkId" in audioBuffer; } function encodeTranslationAudioRequest(url, translationId, audioBuffer, partialAudio) { if (partialAudio && isPartialAudioBuffer(audioBuffer)) return VideoTranslationAudioRequest.encode({ url, translationId, partialAudioInfo: { ...partialAudio, audioBuffer } }).finish(); return VideoTranslationAudioRequest.encode({ url, translationId, audioInfo: audioBuffer }).finish(); } function decodeTranslationAudioResponse(response) { return VideoTranslationAudioResponse.decode(new Uint8Array(response)); } function encodeSubtitlesRequest(url, requestLang) { return SubtitlesRequest.encode({ url, language: requestLang }).finish(); } function decodeSubtitlesResponse(response) { return SubtitlesResponse.decode(new Uint8Array(response)); } function encodeStreamPingRequest(pingId) { return StreamPingRequest.encode({ pingId }).finish(); } function encodeStreamRequest(url, requestLang, responseLang) { return StreamTranslationRequest.encode({ url, language: requestLang, responseLanguage: responseLang, unknown0: 1, unknown1: 0 }).finish(); } function decodeStreamResponse(response) { return StreamTranslationResponse.decode(new Uint8Array(response)); } function encodeVideoLangCacheRequest(url, title) { return VideoLangCacheRequest.encode({ url, title }).finish(); } function decodeVideoLangCacheResponse(response) { return VideoLangCacheResponse.decode(new Uint8Array(response)); } var YandexVOTProtobuf = { encodeTranslationRequest, decodeTranslationResponse, encodeTranslationCacheRequest, decodeTranslationCacheResponse, isPartialAudioBuffer, encodeTranslationAudioRequest, decodeTranslationAudioResponse, encodeSubtitlesRequest, decodeSubtitlesResponse, encodeStreamPingRequest, encodeStreamRequest, decodeStreamResponse, encodeVideoLangCacheRequest, decodeVideoLangCacheResponse }; function encodeSessionRequest(uuid, module) { return YandexSessionRequest.encode({ uuid, module }).finish(); } function decodeSessionResponse(response) { return YandexSessionResponse.decode(new Uint8Array(response)); } var YandexSessionProtobuf = { encodeSessionRequest, decodeSessionResponse }; //#endregion //#region node_modules/@vot.js/core/dist/types/providers/yandex.js var VideoTranslationStatus; (function(VideoTranslationStatus) { VideoTranslationStatus[VideoTranslationStatus["FAILED"] = 0] = "FAILED"; VideoTranslationStatus[VideoTranslationStatus["FINISHED"] = 1] = "FINISHED"; VideoTranslationStatus[VideoTranslationStatus["WAITING"] = 2] = "WAITING"; VideoTranslationStatus[VideoTranslationStatus["LONG_WAITING"] = 3] = "LONG_WAITING"; VideoTranslationStatus[VideoTranslationStatus["PART_CONTENT"] = 5] = "PART_CONTENT"; VideoTranslationStatus[VideoTranslationStatus["AUDIO_REQUESTED"] = 6] = "AUDIO_REQUESTED"; /** * requires login to yandex account */ VideoTranslationStatus[VideoTranslationStatus["SESSION_REQUIRED"] = 7] = "SESSION_REQUIRED"; })(VideoTranslationStatus || (VideoTranslationStatus = {})); var AudioDownloadType; (function(AudioDownloadType) { AudioDownloadType["WEB_API_VIDEO_SRC_FROM_IFRAME"] = "web_api_video_src_from_iframe"; AudioDownloadType["WEB_API_VIDEO_SRC"] = "web_api_video_src"; AudioDownloadType["WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME"] = "web_api_get_all_generating_urls_data_from_iframe"; AudioDownloadType["WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME_TMP_EXP"] = "web_api_get_all_generating_urls_data_from_iframe_tmp_exp"; AudioDownloadType["WEB_API_REPLACED_FETCH_INSIDE_IFRAME"] = "web_api_replaced_fetch_inside_iframe"; AudioDownloadType["ANDROID_API"] = "android_api"; AudioDownloadType["WEB_API_SLOW"] = "web_api_slow"; AudioDownloadType["WEB_API_STEAL_SIG_AND_N"] = "web_api_steal_sig_and_n"; AudioDownloadType["WEB_API_COMBINED"] = "web_api_get_all_generating_urls_data_from_iframe,web_api_steal_sig_and_n"; AudioDownloadType["WEB_ABR"] = "web_abr"; AudioDownloadType["WEB_SABR"] = "web_sabr"; AudioDownloadType["WEB_MSE_PROXY"] = "web_mse_proxy"; AudioDownloadType["EMPTY_PLUG"] = "empty_plug"; })(AudioDownloadType || (AudioDownloadType = {})); //#endregion //#region node_modules/@vot.js/core/dist/providers/yandex.js var YandexProvider = class extends BaseProvider { /** * Headers for interacting with Yandex API */ headers = { "User-Agent": this.userAgent, Accept: "application/x-protobuf", "Accept-Language": "en", "Content-Type": "application/x-protobuf", Pragma: "no-cache", "Cache-Control": "no-cache" }; paths = { videoTranslation: "/video-translation/translate", videoTranslationFailAudio: "/video-translation/fail-audio-js", videoTranslationAudio: "/video-translation/audio", videoTranslationCache: "/video-translation/cache", videoSubtitles: "/video-subtitles/get-subtitles", streamPing: "/stream-translation/ping-stream", streamTranslation: "/stream-translation/translate-stream" }; sessions = {}; apiToken; constructor({ apiToken, ...baseOpts } = {}) { super(baseOpts); this.apiToken = apiToken; } get apiTokenHeader() { if (!this.apiToken) return {}; return { Authorization: `OAuth ${this.apiToken}` }; } mergeHeaders(...headers) { return Object.assign({}, ...headers); } async getSession(module) { const timestamp = getTimestamp$1(); const session = this.sessions[module]; if (session && session.timestamp + session.expires > timestamp) return session; const { secretKey, expires, uuid } = await this.createSession(module); this.sessions[module] = { secretKey, expires, timestamp, uuid }; return this.sessions[module]; } async createSession(module) { const uuid = getUUID(); const body = YandexSessionProtobuf.encodeSessionRequest(uuid, module); const res = await this.request("/session/create", body, this.mergeHeaders({ "Vtrans-Signature": await getSignature(body) })); if (!res.success) throw new VOTJSError("Failed to request create session", res); return { ...YandexSessionProtobuf.decodeSessionResponse(res.data), uuid }; } async requestVtransFailAudio(url, fetchOpts = {}) { const res = await this.requestJSON(this.paths.videoTranslationFailAudio, JSON.stringify({ video_url: url }), this.mergeHeaders({ Accept: "application/json" }), "PUT", fetchOpts); if (!res.data || typeof res.data === "string" || res.data.status !== 1) throw new VOTJSError("Failed to request to fake video translation fail audio js", res); return res; } async translateVideo({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, translationHelp = null, headers = {}, extraOpts = {}, shouldSendFailedAudio = true, fetchOpts = {} }) { const { url, duration = config_default$1.defaultDuration } = videoData; const session = await this.getSession("video-translation"); const body = YandexVOTProtobuf.encodeTranslationRequest(url, duration, requestLang, responseLang, translationHelp, extraOpts); const path = this.paths.videoTranslation; const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path); const apiTokenHeader = extraOpts.useLivelyVoice ? this.apiTokenHeader : {}; const res = await this.request(path, body, this.mergeHeaders(vtransHeaders, apiTokenHeader, headers), void 0, fetchOpts); if (!res.success) throw new VOTJSError("Failed to request video translation", res); const translationData = YandexVOTProtobuf.decodeTranslationResponse(res.data); Logger$1.log("translateVideo", translationData); const { status, translationId } = translationData; switch (status) { case VideoTranslationStatus.FAILED: throw new VOTJSError("Yandex couldn't translate video", translationData); case VideoTranslationStatus.FINISHED: case VideoTranslationStatus.PART_CONTENT: if (!translationData.url) throw new VOTJSError("Audio link wasn't received from Yandex response", translationData); return { translationId, translated: true, url: translationData.url, status, remainingTime: translationData.remainingTime ?? -1 }; case VideoTranslationStatus.WAITING: case VideoTranslationStatus.LONG_WAITING: /** LONG_WAITING: Иногда, в ответе приходит статус код 3, но видео всё, так же, ожидает перевода. В конечном итоге, это занимает слишком много времени, как-будто сервер не понимает, что данное видео уже недавно было переведено и заместо возвращения готовой ссылки на перевод начинает переводить видео заново при чём у него это получается за очень длительное время. */ return { translationId, translated: false, status, remainingTime: translationData.remainingTime ?? -1 }; case VideoTranslationStatus.AUDIO_REQUESTED: if (url.startsWith("https://youtu.be/") && shouldSendFailedAudio) { await this.requestVtransFailAudio(url, fetchOpts); await this.requestVtransAudio(url, translationData.translationId, { audioFile: /* @__PURE__ */ new Uint8Array(0), fileId: `fallback-empty-audio:video-translation:${videoData.videoId}` }, void 0, void 0, fetchOpts); return await this.translateVideo({ videoData, requestLang, responseLang, translationHelp, headers, extraOpts, shouldSendFailedAudio: false, fetchOpts }); } return { translationId, translated: false, status, remainingTime: translationData.remainingTime ?? -1 }; case VideoTranslationStatus.SESSION_REQUIRED: throw new VOTJSError("Yandex auth required to translate video. See docs for more info", translationData); default: Logger$1.error("Unknown response", translationData); throw new VOTJSError("Unknown response from Yandex", translationData); } } async requestVtransAudio(url, translationId, audioBuffer, partialAudio, headers = {}, fetchOpts = {}) { const session = await this.getSession("video-translation"); let body; if (YandexVOTProtobuf.isPartialAudioBuffer(audioBuffer)) { if (!partialAudio) throw new VOTJSError("Partial audio metadata is required for partial audio buffer", audioBuffer); body = YandexVOTProtobuf.encodeTranslationAudioRequest(url, translationId, audioBuffer, partialAudio); } else body = YandexVOTProtobuf.encodeTranslationAudioRequest(url, translationId, audioBuffer, void 0); const path = this.paths.videoTranslationAudio; const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path); const res = await this.request(path, body, this.mergeHeaders(vtransHeaders, headers), "PUT", fetchOpts); if (!res.success) throw new VOTJSError("Failed to request video translation audio", res); return YandexVOTProtobuf.decodeTranslationAudioResponse(res.data); } async getSubtitles({ videoData, requestLang = this.requestLang, headers = {}, fetchOpts = {} }) { const { url } = videoData; const session = await this.getSession("video-translation"); const body = YandexVOTProtobuf.encodeSubtitlesRequest(url, requestLang); const path = this.paths.videoSubtitles; const vsubsHeaders = await getSecYaHeaders("Vsubs", session, body, path); const res = await this.request(path, body, this.mergeHeaders(vsubsHeaders, headers), void 0, fetchOpts); if (!res.success) throw new VOTJSError("Failed to request video subtitles", res); const subtitlesData = YandexVOTProtobuf.decodeSubtitlesResponse(res.data); const subtitles = subtitlesData.subtitles.map((subtitle) => { const { language, url, translatedLanguage, translatedUrl } = subtitle; return { language, url, translatedLanguage, translatedUrl }; }); return { waiting: subtitlesData.waiting, subtitles }; } /** * @includeExample examples/stream.ts[7:44] */ async pingStream({ pingId, headers = {}, fetchOpts = {} }) { const session = await this.getSession("video-translation"); const body = YandexVOTProtobuf.encodeStreamPingRequest(pingId); const path = this.paths.streamPing; const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path); const res = await this.request(path, body, this.mergeHeaders(vtransHeaders, headers), void 0, fetchOpts); if (!res.success) throw new VOTJSError("Failed to request stream ping", res); return true; } /** * @includeExample examples/stream.ts[7:44] */ async translateStream({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, headers = {}, fetchOpts = {} }) { const { url } = videoData; if (isCustomLink(url)) throw new VOTJSError("Unsupported video URL for getting stream translation"); const session = await this.getSession("video-translation"); const body = YandexVOTProtobuf.encodeStreamRequest(url, requestLang, responseLang); const path = this.paths.streamTranslation; const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path); const res = await this.request(path, body, this.mergeHeaders(vtransHeaders, headers), void 0, fetchOpts); if (!res.success) throw new VOTJSError("Failed to request stream translation", res); const translateResponse = YandexVOTProtobuf.decodeStreamResponse(res.data); const interval = translateResponse.interval; switch (interval) { case StreamInterval.NO_CONNECTION: case StreamInterval.TRANSLATING: return { translated: false, interval, message: interval === StreamInterval.NO_CONNECTION ? "streamNoConnectionToServer" : "translationTakeFewMinutes" }; case StreamInterval.STREAMING: if (translateResponse.pingId === void 0) throw new VOTJSError("Stream ping id wasn't received from Yandex response", translateResponse); if (!translateResponse.translatedInfo) throw new VOTJSError("Stream translation info wasn't received from Yandex response", translateResponse); return { translated: true, interval, pingId: translateResponse.pingId, result: translateResponse.translatedInfo }; default: Logger$1.error("Unknown response", translateResponse); throw new VOTJSError("Unknown response from Yandex", translateResponse); } } async translateVideoCache({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, headers = {}, fetchOpts = {} }) { const { url, duration = config_default$1.defaultDuration } = videoData; const session = await this.getSession("video-translation"); const body = YandexVOTProtobuf.encodeTranslationCacheRequest(url, duration, requestLang, responseLang); const path = this.paths.videoTranslationCache; const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path); const res = await this.request(path, body, this.mergeHeaders(vtransHeaders, headers), "POST", fetchOpts); if (!res.success) throw new VOTJSError("Failed to request video translation cache", res); return YandexVOTProtobuf.decodeTranslationCacheResponse(res.data); } }; //#endregion //#region node_modules/@vot.js/core/dist/providers/votworker.js var VOTNextWorkerProvider = class extends YandexProvider { constructor(opts = {}) { opts.host = opts.host ?? config_default$1.hostWorker; super(opts); } mergeHeaders(...headers) { const data = Object.assign({}, this.headers, ...headers); try { return { "User-Agent": `vot.js/${config_default$1.version}`, "X-VOT-Headers": btoa(JSON.stringify(data)) }; } catch { throw new VOTJSError("Failed to encode headers for VOT Worker request"); } } }; //#endregion //#region node_modules/@vot.js/ext/dist/client.js var VOTClient = class extends VOTClient$1 { constructor(opts) { super(opts); this.setHeaders(); } setHeaders() { this.provider.headers = { ...browserSecHeaders, ...this.provider.headers }; return this; } }; //#endregion //#region node_modules/chaimu/dist/config.js var config_default = { version: "1.1.0", debug: false, fetchFn: fetch.bind(window) }; //#endregion //#region node_modules/chaimu/dist/debug.js var debug_default = { log: (...text) => { if (!config_default.debug) return; return console.log(`%c✦ chaimu.js v${config_default.version} ✦`, "background: #000; color: #fff; padding: 0 8px", ...text); } }; //#endregion //#region node_modules/chaimu/dist/player.js var videoLipSyncEvents = [ "playing", "ratechange", "play", "waiting", "pause", "seeked", "ended", "timeupdate" ]; function initAudioContext() { const audioContext = window.AudioContext || window.webkitAudioContext; return audioContext ? new audioContext() : void 0; } var BasePlayer = class { static name = "BasePlayer"; chaimu; fetch; _src; _currentSrc; fetchOpts; isDestroyed = false; destructionPromise; storedVolume = 1; lifecycleGeneration = 0; lifecycleQueue = Promise.resolve(); videoWithEvents; constructor(chaimu, src) { this.chaimu = chaimu; this._src = src; this.fetch = this.chaimu.fetchFn; this.fetchOpts = this.chaimu.fetchOpts; } async init() { return this; } async clear() { return this; } destroy() { if (this.destructionPromise) return this.destructionPromise; this.isDestroyed = true; this.removeVideoEvents(); this.destructionPromise = (async () => { await this.clear(); await this.closeAudioContext(); return this; })(); return this.destructionPromise; } assertActive() { if (this.isDestroyed) throw new Error(`${this.name} has been destroyed`); } async closeAudioContext() { const audioContext = this.chaimu.audioContext; if (!audioContext) return; try { if (audioContext.state !== "closed") await audioContext.close(); } finally { if (this.chaimu.audioContext === audioContext) this.chaimu.audioContext = void 0; } } unloadMediaElement(mediaElement) { if (!mediaElement) return; mediaElement.pause(); mediaElement.src = ""; mediaElement.removeAttribute("src"); mediaElement.load(); } composeFetchSignal(lifecycleSignal) { const callerSignal = this.fetchOpts?.signal; if (!(callerSignal instanceof AbortSignal)) return lifecycleSignal; if (!lifecycleSignal || callerSignal === lifecycleSignal) return callerSignal; return AbortSignal.any([callerSignal, lifecycleSignal]); } enqueueLifecycle(operation) { const result = this.lifecycleQueue.then(operation); this.lifecycleQueue = result.then(() => void 0, () => void 0); return result; } isVideoPlaying() { const video = this.chaimu.video; return !video.paused && !video.ended && video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA; } lipSync(_mode = false) { return this; } handleVideoEvent = (event) => { if (this.isDestroyed || event.currentTarget !== this.chaimu.video) return this; debug_default.log(`handle video ${event.type}`); if (event.type === "timeupdate") { if (this.playbackRate !== this.chaimu.video.playbackRate) this.playbackRate = this.chaimu.video.playbackRate; return this; } this.lipSync(event.type); return this; }; audioErrorHandle = (error) => { console.error(`[${this.name}]`, error); }; removeVideoEvents(video = this.videoWithEvents ?? this.chaimu.video) { for (const e of videoLipSyncEvents) video.removeEventListener(e, this.handleVideoEvent); if (this.videoWithEvents === video) this.videoWithEvents = void 0; return this; } addVideoEvents(video = this.chaimu.video) { this.assertActive(); if (this.videoWithEvents === video) return this; if (this.videoWithEvents) this.removeVideoEvents(this.videoWithEvents); for (const e of videoLipSyncEvents) video.addEventListener(e, this.handleVideoEvent); this.videoWithEvents = video; return this; } async play() { return this; } async pause() { return this; } get name() { return this.constructor.name; } set src(url) { this._src = url; } get src() { return this._src; } get currentSrc() { return this._currentSrc; } set volume(_value) {} get volume() { return 0; } get playbackRate() { return 0; } set playbackRate(_value) {} get currentTime() { return 0; } }; var AudioPlayer = class extends BasePlayer { static name = "AudioPlayer"; audio; gainNode; audioSource; constructor(chaimu, src) { super(chaimu, src); this.updateAudio(); } initAudioBooster() { if (!this.chaimu.audioContext) return this; this.disconnectAudioNodes(); this.gainNode = this.chaimu.audioContext.createGain(); this.gainNode.gain.value = this.storedVolume; this.gainNode.connect(this.chaimu.audioContext.destination); this.audioSource = this.chaimu.audioContext.createMediaElementSource(this.audio); this.audioSource.connect(this.gainNode); return this; } disconnectAudioNodes() { if (this.audioSource) { this.audioSource.disconnect(); this.audioSource = void 0; } if (this.gainNode) { this.gainNode.disconnect(); this.gainNode = void 0; } } updateAudio() { this.lifecycleGeneration += 1; this.disconnectAudioNodes(); this.unloadMediaElement(this.audio); this.audio = new Audio(this.src); this.audio.crossOrigin = "anonymous"; this.audio.playbackRate = this.chaimu.video.playbackRate; if (!this.chaimu.audioContext) this.audio.volume = this.storedVolume; this._currentSrc = this.src; return this; } async init() { this.assertActive(); this.updateAudio(); this.initAudioBooster(); return this; } lipSync(mode = false) { debug_default.log("[AudioPlayer] lipsync video", this.chaimu.video); if (!this.chaimu.video) return this; if (this._currentSrc) { this.audio.currentTime = this.chaimu.video.currentTime; this.audio.playbackRate = this.chaimu.video.playbackRate; } if (!mode) { debug_default.log("[AudioPlayer] lipsync mode isn't set"); return this; } debug_default.log(`[AudioPlayer] lipsync mode is ${mode}`); switch (mode) { case "playing": if (!this.chaimu.video.paused && !this.chaimu.video.ended) this.syncPlay(); return this; case "seeked": if (this.isVideoPlaying()) this.syncPlay(); else this.pause().catch(this.audioErrorHandle); return this; case "pause": case "waiting": case "ended": this.pause().catch(this.audioErrorHandle); return this; default: return this; } } async clear() { this.lifecycleGeneration += 1; this.disconnectAudioNodes(); this.unloadMediaElement(this.audio); this._currentSrc = void 0; return this; } syncPlay() { debug_default.log("[AudioPlayer] sync play called"); this.play().catch(this.audioErrorHandle); return this; } async play() { this.assertActive(); debug_default.log("[AudioPlayer] play called"); if (!this._src) throw new Error("No audio source provided"); const generation = this.lifecycleGeneration; return this.enqueueLifecycle(async () => { if (generation !== this.lifecycleGeneration) return this; if (this.chaimu.audioContext?.state === "suspended") await this.chaimu.audioContext.resume(); if (generation !== this.lifecycleGeneration) return this; await this.audio.play(); return this; }); } async pause() { this.assertActive(); debug_default.log("[AudioPlayer] pause called"); this.lifecycleGeneration += 1; this.audio.pause(); return this; } set src(url) { this.assertActive(); this._src = url; if (!url) { this.clear(); return; } this.updateAudio(); if (this.chaimu.audioContext) this.initAudioBooster(); } get src() { return this._src; } get currentSrc() { return this._currentSrc; } set volume(value) { this.storedVolume = value; if (this.gainNode) { this.gainNode.gain.value = value; return; } this.audio.volume = value; } get volume() { return this.storedVolume; } get playbackRate() { return this.audio.playbackRate; } set playbackRate(value) { this.audio.playbackRate = value; } get currentTime() { return this._currentSrc ? this.audio.currentTime : 0; } }; var ChaimuPlayer = class extends BasePlayer { static name = "ChaimuPlayer"; audioBuffer; audioElement; mediaElementSource; gainNode; blobUrl; initializationAbortController; cancelInitialization; clearingPromise; playbackGeneration = 0; sourceGeneration = 0; async fetchAudio(signal) { if (!this._src) throw new Error("No audio source provided"); if (!this.chaimu.audioContext) throw new Error("No audio context available"); debug_default.log(`[ChaimuPlayer] Fetching audio from ${this._src}...`); let tempBlobUrl; try { const fetchSignal = this.composeFetchSignal(signal); const res = await this.fetch(this._src, { ...this.fetchOpts, signal: fetchSignal }); fetchSignal?.throwIfAborted(); debug_default.log(`[ChaimuPlayer] Decoding fetched audio...`); const data = await res.arrayBuffer(); fetchSignal?.throwIfAborted(); const blob = new Blob([data]); tempBlobUrl = URL.createObjectURL(blob); const audioBuffer = await this.chaimu.audioContext.decodeAudioData(data); fetchSignal?.throwIfAborted(); if (this.blobUrl) URL.revokeObjectURL(this.blobUrl); this.audioBuffer = audioBuffer; this.blobUrl = tempBlobUrl; tempBlobUrl = void 0; } catch (err) { if (tempBlobUrl) URL.revokeObjectURL(tempBlobUrl); throw new Error(`Failed to fetch audio file, because ${err.message}`); } return this; } initAudioBooster() { if (!this.chaimu.audioContext) return this; this.disconnectAudioNodes(); this.gainNode = this.chaimu.audioContext.createGain(); this.gainNode.gain.value = this.storedVolume; return this; } disconnectAudioNodes() { if (this.mediaElementSource) { this.mediaElementSource.disconnect(); this.mediaElementSource = void 0; } if (this.gainNode) { this.gainNode.disconnect(); this.gainNode = void 0; } } releaseMediaResources() { this.disconnectAudioNodes(); this.unloadMediaElement(this.audioElement); this.audioElement = void 0; this.audioBuffer = void 0; if (this.blobUrl) { URL.revokeObjectURL(this.blobUrl); this.blobUrl = void 0; } this._currentSrc = void 0; } async init() { this.assertActive(); if (!this._src) return this; const generation = this.lifecycleGeneration; return this.enqueueLifecycle(async () => { if (generation !== this.lifecycleGeneration) return this; this.releaseMediaResources(); const abortController = new AbortController(); let cancelInitialization; const cancellation = new Promise((resolve) => { cancelInitialization = () => resolve("cancelled"); }); this.initializationAbortController = abortController; this.cancelInitialization = cancelInitialization; try { const initialization = this.fetchAudio(abortController.signal).then(() => "initialized"); if (await Promise.race([initialization, cancellation]) === "cancelled" || generation !== this.lifecycleGeneration) return this; this.initAudioBooster(); this.createAudioElement(); return this; } finally { if (this.initializationAbortController === abortController) { this.initializationAbortController = void 0; this.cancelInitialization = void 0; } } }); } createAudioElement() { if (!this.chaimu.audioContext) throw new Error("No audio context available"); if (!this.blobUrl) throw new Error("No blob URL available."); const audio = new Audio(this.blobUrl); audio.crossOrigin = "anonymous"; audio.playbackRate = this.chaimu.video.playbackRate; if ("preservesPitch" in audio) { audio.preservesPitch = true; if ("mozPreservesPitch" in audio) audio.mozPreservesPitch = true; if ("webkitPreservesPitch" in audio) audio.webkitPreservesPitch = true; } this.audioElement = audio; this.mediaElementSource = this.chaimu.audioContext.createMediaElementSource(audio); this.mediaElementSource.connect(this.gainNode); this.gainNode.connect(this.chaimu.audioContext.destination); this._currentSrc = this._src; } lipSync(mode = false) { debug_default.log("[ChaimuPlayer] lipsync video", this.chaimu.video, this); if (!this.chaimu.video) return this; if (this.audioElement) { this.audioElement.currentTime = this.chaimu.video.currentTime; this.audioElement.playbackRate = this.chaimu.video.playbackRate; } if (!mode) { debug_default.log("[ChaimuPlayer] lipsync mode isn't set"); return this; } debug_default.log(`[ChaimuPlayer] lipsync mode is ${mode}`); switch (mode) { case "playing": if (!this.chaimu.video.paused && !this.chaimu.video.ended) this.play().catch(this.audioErrorHandle); return this; case "seeked": if (this.isVideoPlaying()) this.play().catch(this.audioErrorHandle); else this.pause().catch(this.audioErrorHandle); return this; case "pause": case "waiting": case "ended": this.pause().catch(this.audioErrorHandle); return this; default: return this; } } async reopenCtx() { if (!this.chaimu.audioContext) throw new Error("No audio context available"); try { if (this.chaimu.audioContext.state !== "closed") await this.chaimu.audioContext.close(); } catch (err) { debug_default.log("[ChaimuPlayer] Failed to close audio context:", err); } this.chaimu.audioContext = initAudioContext(); return this; } async clear() { if (this.isDestroyed) return await this.destructionPromise ?? this; this.sourceGeneration += 1; this._currentSrc = void 0; if (this.clearingPromise) return this.clearingPromise; if (!this.chaimu.audioContext) throw new Error("No audio context available"); debug_default.log("clear audio context"); this.lifecycleGeneration += 1; this.initializationAbortController?.abort(); this.cancelInitialization?.(); const clearingPromise = this.enqueueLifecycle(async () => { this.releaseMediaResources(); await this.reopenCtx(); return this; }); this.clearingPromise = clearingPromise; try { return await clearingPromise; } finally { if (this.clearingPromise === clearingPromise) this.clearingPromise = void 0; } } destroy() { if (this.destructionPromise) return this.destructionPromise; this.isDestroyed = true; this.removeVideoEvents(); this._currentSrc = void 0; this.sourceGeneration += 1; this.lifecycleGeneration += 1; this.initializationAbortController?.abort(); this.cancelInitialization?.(); this.destructionPromise = this.enqueueLifecycle(async () => { this.releaseMediaResources(); await this.closeAudioContext(); return this; }); return this.destructionPromise; } async play() { this.assertActive(); if (!this._src) throw new Error("No audio source provided"); if (this.clearingPromise) await this.clearingPromise; const generation = this.lifecycleGeneration; const playbackGeneration = this.playbackGeneration; return this.enqueueLifecycle(async () => { if (generation !== this.lifecycleGeneration || playbackGeneration !== this.playbackGeneration) return this; if (!this.chaimu.audioContext) throw new Error("No audio context available"); if (!this.audioElement) throw new Error("Audio element is missing"); debug_default.log("starting audio via HTMLAudioElement"); if (this.chaimu.audioContext.state === "suspended") await this.chaimu.audioContext.resume(); if (generation !== this.lifecycleGeneration || playbackGeneration !== this.playbackGeneration) return this; const audioElement = this.audioElement; if (!audioElement) return this; if (this.chaimu.video) { audioElement.currentTime = this.chaimu.video.currentTime; audioElement.playbackRate = this.chaimu.video.playbackRate; } await audioElement.play(); return this; }); } start() { return this.play(); } async pause() { this.assertActive(); this.playbackGeneration += 1; if (this.audioElement) this.audioElement.pause(); return this; } set src(url) { this.assertActive(); this._src = url; const clearing = this.clear(); const sourceGeneration = this.sourceGeneration; if (!url) { clearing.catch((err) => debug_default.log("[ChaimuPlayer] Failed to clear source:", err)); return; } clearing.then(() => { if (sourceGeneration !== this.sourceGeneration || this._src !== url) return this; return this.init(); }).catch((err) => debug_default.log("[ChaimuPlayer] Failed to replace source:", err)); } get src() { return this._src; } get currentSrc() { return this._currentSrc; } set volume(value) { this.storedVolume = value; if (this.gainNode) this.gainNode.gain.value = value; } get volume() { return this.storedVolume; } set playbackRate(value) { if (this.audioElement) this.audioElement.playbackRate = value; } get playbackRate() { return this.audioElement ? this.audioElement.playbackRate : this.chaimu.video?.playbackRate ?? 1; } get currentTime() { return this.audioElement?.currentTime ?? 0; } }; //#endregion //#region node_modules/chaimu/dist/client.js var Chaimu = class { _debug = false; audioContext; isDestroyed = false; isInitialized = false; destructionPromise; lifecycleQueue; player; video; fetchFn; fetchOpts; constructor({ url, video, debug = false, fetchFn = config_default.fetchFn, fetchOpts = {}, preferAudio = false }) { this._debug = config_default.debug = debug; this.fetchFn = fetchFn; this.fetchOpts = fetchOpts; this.video = video; this.audioContext = initAudioContext(); this.player = this.audioContext && !preferAudio ? new ChaimuPlayer(this, url) : new AudioPlayer(this, url); } assertActive() { if (this.isDestroyed) throw new Error("Chaimu has been destroyed"); } isVideoPlaying(video) { return !video.paused && !video.ended && video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA; } enqueueLifecycle(operation) { let result; if (this.lifecycleQueue) result = this.lifecycleQueue.then(operation); else try { result = Promise.resolve(operation()); } catch (error) { result = Promise.reject(error instanceof Error ? error : new Error(String(error))); } const lifecycleQueue = result.then(() => void 0, () => void 0); this.lifecycleQueue = lifecycleQueue; lifecycleQueue.then(() => { if (this.lifecycleQueue === lifecycleQueue) this.lifecycleQueue = void 0; }); return result; } async init() { this.assertActive(); return this.enqueueLifecycle(async () => { this.assertActive(); await this.player.init(); if (this.isDestroyed) return; if (this.isVideoPlaying(this.video)) this.player.lipSync("playing"); this.player.addVideoEvents(); this.isInitialized = true; }); } async replaceVideo(newVideo) { this.assertActive(); return this.enqueueLifecycle(() => { this.assertActive(); if (newVideo === this.video) return this; this.player.removeVideoEvents(); this.video = newVideo; if (this.isInitialized) this.player.addVideoEvents(); this.player.lipSync(this.isVideoPlaying(newVideo) ? "seeked" : "pause"); return this; }); } destroy() { if (this.destructionPromise) return this.destructionPromise; this.isDestroyed = true; this.destructionPromise = this.player.destroy().then(() => this); return this.destructionPromise; } get destroyed() { return this.isDestroyed; } set debug(value) { this._debug = config_default.debug = value; } get debug() { return this._debug; } }; //#endregion //#region src/core/cacheManager.ts var YANDEX_TTL_MS = 72e5; var VOT_SESSION_STORAGE_KEY = "VOTSession"; function getCurrentUnixTimestampSeconds() { return Math.floor(Date.now() / 1e3); } function isClientSession(value) { if (!value || typeof value !== "object") return false; const candidate = value; return typeof candidate.expires === "number" && Number.isFinite(candidate.expires) && typeof candidate.timestamp === "number" && Number.isFinite(candidate.timestamp) && typeof candidate.uuid === "string" && candidate.uuid.length > 0 && typeof candidate.secretKey === "string" && candidate.secretKey.length > 0; } function sanitizeVOTSessions(value) { if (!value || typeof value !== "object") return {}; const now = getCurrentUnixTimestampSeconds(); const entries = Object.entries(value).flatMap(([module, session]) => { if (!isClientSession(session)) return []; if (session.timestamp + session.expires <= now) return []; return [[module, session]]; }); return Object.fromEntries(entries); } function hasSessions(sessions) { return Object.keys(sessions).length > 0; } var VOTSessionStorageCache = class { storage; constructor(storage = votStorage) { this.storage = storage; } getStorageKey() { return VOT_SESSION_STORAGE_KEY; } async restore(_host, currentSessions = {}) { const storageKey = this.getStorageKey(); const rawStoredSession = await this.storage.getRaw(storageKey); const restoredSessions = sanitizeVOTSessions(rawStoredSession); if (!hasSessions(restoredSessions)) { if (rawStoredSession !== void 0) await this.storage.deleteRaw(storageKey); return currentSessions; } return { ...currentSessions, ...restoredSessions }; } async persist(_host, sessions) { const storageKey = this.getStorageKey(); const sanitizedSessions = sanitizeVOTSessions(sessions); if (!hasSessions(sanitizedSessions)) { await this.storage.deleteRaw(storageKey); return; } await this.storage.setRaw(storageKey, sanitizedSessions); } }; /** * Small in-memory cache with TTL for both translations and subtitles. * * The cache is keyed by a stable key built by VideoHandler. */ var InMemoryCacheManager = class { translations = /* @__PURE__ */ new Map(); subtitles = /* @__PURE__ */ new Map(); /** * Clears all cached entries. * * Used when runtime settings change (e.g. proxy mode/host), because cached * translation URLs and especially previous failures can become stale. */ clear() { this.translations.clear(); this.subtitles.clear(); } getTranslation(key) { return this.getFreshValue(this.translations, key); } setTranslation(key, translation) { this.setFreshValue(this.translations, key, translation); } getSubtitles(key) { return this.getFreshValue(this.subtitles, key); } setSubtitles(key, subtitles) { this.setFreshValue(this.subtitles, key, subtitles); } deleteSubtitles(key) { this.subtitles.delete(key); } getFreshValue(cache, key) { const entry = cache.get(key); if (!entry) return void 0; if (entry.expiresAt <= Date.now()) { cache.delete(key); return; } return entry.value; } setFreshValue(cache, key, value) { cache.set(key, { value, expiresAt: computeExpiresAt(Date.now(), YANDEX_TTL_MS) }); } }; //#endregion //#region src/core/fullscreenHelper.ts var FullscreenHelper = class { container; video; fullscreenChangeListeners = /* @__PURE__ */ new Set(); handleFullscreenChange = () => { this.notifyFullscreenChange(); }; nativeFullscreenListenersActive = false; constructor({ container, video }) { this.container = container; this.video = video; } /** * Gets the current fullscreen element with proper ShadowDOM support */ getFullscreenElement() { const doc = document; const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; if (!(fullscreenEl instanceof HTMLElement)) return null; return fullscreenEl; } /** * Gets comprehensive fullscreen information including ShadowDOM details */ getFullscreenInfo() { const element = this.getFullscreenElement(); const isFullscreen = Boolean(element); if (!element) return { element: null, shadowRoot: null, isFullscreen: false, belongsToCurrentVideo: false }; return { element, shadowRoot: element.shadowRoot ?? null, isFullscreen, belongsToCurrentVideo: this.isElementBelongsToCurrentVideo(element) }; } /** * Checks if the given element belongs to the current video/container */ isElementBelongsToCurrentVideo(element) { return element === this.container || containsCrossShadow(element, this.container) || containsCrossShadow(this.container, element) || this.video && (element === this.video || containsCrossShadow(element, this.video) || containsCrossShadow(this.video, element)); } /** * Gets the appropriate root element for overlay mounting in fullscreen mode * For Shadow DOM players (e.g., Reddit's shreddit-player), returns shadowRoot * to ensure UI is mounted inside the shadow tree, not in the light DOM. */ getOverlayRoot() { const { element, belongsToCurrentVideo, shadowRoot } = this.getFullscreenInfo(); if (!element || !belongsToCurrentVideo) return null; return shadowRoot ?? element; } /** * Gets the appropriate element for ResizeObserver to watch for size changes * Handles both regular DOM and ShadowDOM scenarios */ getResizeObserverTarget() { const { element, belongsToCurrentVideo, shadowRoot } = this.getFullscreenInfo(); if (element && belongsToCurrentVideo) return shadowRoot?.host ?? element; return this.container; } /** * Checks if the current container should be considered "big" for button positioning * Takes into account fullscreen state and ShadowDOM */ isBigContainer(threshold = 550) { const target = this.getResizeObserverTarget(); const rect = target.getBoundingClientRect(); const videoRect = this.video?.getBoundingClientRect(); let width = target.clientWidth; if (rect.width > 0) width = rect.width; if (videoRect && videoRect.width < rect.width) width = videoRect.width; return width > threshold; } /** * Adds a listener for fullscreen changes */ addFullscreenChangeListener(listener) { this.fullscreenChangeListeners.add(listener); if (this.fullscreenChangeListeners.size === 1) this.setupFullscreenListeners(); } /** * Removes a fullscreen change listener */ removeFullscreenChangeListener(listener) { this.fullscreenChangeListeners.delete(listener); if (this.fullscreenChangeListeners.size === 0) this.cleanupFullscreenListeners(); } /** * Sets up native fullscreen event listeners */ setupFullscreenListeners() { if (this.nativeFullscreenListenersActive) return; document.addEventListener("fullscreenchange", this.handleFullscreenChange); document.addEventListener("webkitfullscreenchange", this.handleFullscreenChange); if (this.video) { this.video.addEventListener("webkitbeginfullscreen", this.handleFullscreenChange); this.video.addEventListener("webkitendfullscreen", this.handleFullscreenChange); } this.nativeFullscreenListenersActive = true; } /** * Cleans up fullscreen event listeners */ cleanupFullscreenListeners() { if (!this.nativeFullscreenListenersActive) return; document.removeEventListener("fullscreenchange", this.handleFullscreenChange); document.removeEventListener("webkitfullscreenchange", this.handleFullscreenChange); if (this.video) { this.video.removeEventListener("webkitbeginfullscreen", this.handleFullscreenChange); this.video.removeEventListener("webkitendfullscreen", this.handleFullscreenChange); } this.nativeFullscreenListenersActive = false; } /** * Notifies all listeners about fullscreen state changes */ notifyFullscreenChange() { for (const listener of this.fullscreenChangeListeners) try { listener(); } catch (error) { console.warn("[FullscreenHelper] Error in fullscreen change listener:", error); } } /** * Updates the container reference (useful when video container changes) */ updateContainer(container) { this.container = container; } /** * Updates the video reference */ updateVideo(video) { const shouldRebind = this.nativeFullscreenListenersActive && this.video !== video; if (shouldRebind) this.cleanupFullscreenListeners(); this.video = video; if (shouldRebind && this.fullscreenChangeListeners.size > 0) this.setupFullscreenListeners(); } /** * Cleans up all resources */ destroy() { this.cleanupFullscreenListeners(); this.fullscreenChangeListeners.clear(); } }; //#endregion //#region src/core/overlayMountTargets.ts function resolveOverlayBaseContainer(container, site) { return site.host === "youtube" && site.additionalData !== "mobile" ? container.parentElement ?? container : container; } function resolveOverlayMountTargets(input) { const base = resolveOverlayBaseContainer(input.container, input.site); const root = input.fullscreenRoot ?? base; return { base, root, portalContainer: base, subtitlesMountContainer: root }; } //#endregion //#region src/core/translateApis.ts var SETTINGS_CACHE_TTL_MS = 5e3; var IMMUTABLE_LOOKUP_CACHE_TTL_MS = Number.MAX_SAFE_INTEGER; var cachedTranslationService = null; var cachedTranslationServiceAt = 0; var cachedDetectService = null; var cachedDetectServiceAt = 0; async function getTranslationServiceCached() { const now = Date.now(); if (cachedTranslationService && now - cachedTranslationServiceAt < SETTINGS_CACHE_TTL_MS) return cachedTranslationService; const service = await votStorage.get("translationService", defaultTranslationService); cachedTranslationService = String(service); cachedTranslationServiceAt = now; return cachedTranslationService; } async function getDetectServiceCached() { const now = Date.now(); if (cachedDetectService && now - cachedDetectServiceAt < SETTINGS_CACHE_TTL_MS) return cachedDetectService; const service = await votStorage.get("detectService", defaultDetectService); cachedDetectService = String(service); cachedDetectServiceAt = now; return cachedDetectService; } var foswlyServices = ["yandexbrowser", "msedge"]; /** * Limit: 10k symbols for yandex, 50k for msedge */ var FOSWLYTranslateAPI = new class { isFOSWLYError(data) { return Object.hasOwn(data, "error"); } async request(path, opts = {}) { try { const data = await (await GM_fetch(`${foswlyTranslateUrl}${path}`, { timeout: 3e3, responseCache: { ttlMs: IMMUTABLE_LOOKUP_CACHE_TTL_MS, cacheName: "vot-foswly-api-v1", allowStaleOnError: true }, ...opts })).json(); if (this.isFOSWLYError(data)) throw new Error(data.error); return data; } catch (err) { console.error(`[VOT] Failed to get data from FOSWLY Translate API, because ${err instanceof Error ? err.message : String(err)}`); return; } } async translateMultiple(text, lang, service) { const result = await this.request("/translate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, lang, service }) }); return result ? result.translations : text; } async translate(text, lang, service) { const result = await this.request(`/translate?${new URLSearchParams({ text, lang, service })}`); return result ? result.translations[0] : text; } async detect(text, service) { const result = await this.request(`/detect?${new URLSearchParams({ text, service })}`); return result ? result.lang : "en"; } }(); var RustServerAPI = { async detect(text) { try { return await (await GM_fetch(detectRustServerUrl, { method: "POST", body: text, timeout: 3e3, responseCache: { ttlMs: IMMUTABLE_LOOKUP_CACHE_TTL_MS, cacheName: "vot-rust-detect-v1", allowStaleOnError: true } })).text(); } catch (error) { console.error(`[VOT] Error getting lang from text, because ${error.message}`); return "en"; } } }; async function translate(text, fromLang = "", toLang = "ru") { if (fromLang && toLang && fromLang === toLang) return text; const service = await getTranslationServiceCached(); switch (service) { case "yandexbrowser": case "msedge": { const langPair = fromLang && toLang ? `${fromLang}-${toLang}` : toLang; return Array.isArray(text) ? await FOSWLYTranslateAPI.translateMultiple(text, langPair, service) : await FOSWLYTranslateAPI.translate(text, langPair, service); } default: return text; } } async function detect(text) { const service = await getDetectServiceCached(); switch (service) { case "yandexbrowser": case "msedge": return await FOSWLYTranslateAPI.detect(text, service); case "rust-server": return await RustServerAPI.detect(text); default: return "en"; } } var detectServices = [...foswlyServices, "rust-server"]; //#endregion //#region src/audioDownloader/strategies/audioStrategy.ts /** * SABR is intentionally a distinct internal strategy. It is not folded into * WEB_ABR: this keeps selection and diagnostics honest while avoiding a hard * dependency on a core enum value that may not exist yet. */ var SABR_STRATEGY = AudioDownloadType.WEB_SABR; var WEB_ABR_STRATEGY = AudioDownloadType.WEB_ABR; var WEB_MSE_PROXY_STRATEGY = AudioDownloadType.WEB_MSE_PROXY; function isAudioBridgeStrategy(value) { return value === SABR_STRATEGY || value === WEB_ABR_STRATEGY || value === WEB_MSE_PROXY_STRATEGY; } //#endregion //#region src/audioDownloader/strategies/webAudioBridge.ts var MESSAGE_TYPE$1 = "get-audio-chunks-by-mse-in-main-world"; var STREAM_TIMEOUT_MS = 18e5; var MESSAGE_TIMEOUT_MS = 3e5; function parseAudioBridgeChunk(payload) { if (!payload || typeof payload !== "object" || !("buffer" in payload)) throw new Error("Audio downloader. Invalid audio bridge chunk"); const { buffer, isLastChunk } = payload; const bytes = buffer instanceof Uint8Array ? buffer : buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : ArrayBuffer.isView(buffer) ? new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) : null; if (!bytes || typeof isLastChunk !== "boolean") throw new Error("Audio downloader. Invalid audio bridge chunk"); return { buffer: bytes, isLastChunk }; } async function* getAudioBridgeChunks(videoId, signal, audioDownloadType, sourceLanguage) { if (signal.aborted) throw makeAbortError(signal.reason); const messageId = `stream-message-id-${performance.now()}-${Math.random()}`; const chunks = []; let wake; let streamFinished = false; let failure; let receivedChunks = 0; let messageTimeout; const notify = () => { wake?.(); wake = void 0; }; const finish = (error) => { if (error) { if (failure) return; failure = error; debug.error("Audio downloader. Audio bridge failed", { videoId, sourceLanguage, messageId, audioDownloadType, receivedChunks, error: error.message }); } else { streamFinished = true; clearTimeout(messageTimeout); debug.log("Audio downloader. Audio bridge stream finished", { videoId, sourceLanguage, messageId, audioDownloadType, receivedChunks }); } notify(); }; const resetMessageTimeout = () => { clearTimeout(messageTimeout); messageTimeout = setTimeout(() => finish(/* @__PURE__ */ new Error("Audio bridge message timed out")), MESSAGE_TIMEOUT_MS); }; const throwIfFailed = () => { if (!failure) return; if (!globalThis.location.href.includes(videoId)) throw makeAbortError("URL changed during audio download"); throw failure; }; const postAbort = () => globalThis.postMessage({ messageId, messageType: MESSAGE_TYPE$1, messageDirection: "request", isStreamFinished: true, isAborted: true }, "*"); const onMessage = (event) => { const message = event.data; const expectedOrigin = globalThis.location.origin; if (!message || event.origin !== expectedOrigin || message.messageId !== messageId || message.messageType !== MESSAGE_TYPE$1 || message.messageDirection !== "response") return; resetMessageTimeout(); if (message.isAborted) { finish(makeAbortError(message.error)); return; } if (message.error) { finish(new Error(typeof message.error === "string" ? message.error : "Audio bridge failed")); return; } if (message.isStreamFinished) { finish(); return; } if (message.isProgress) { debug.log("Audio downloader. Audio bridge progress", { videoId, messageId, audioDownloadType }); return; } try { const chunk = parseAudioBridgeChunk(message.payload); chunks.push(chunk); receivedChunks++; debug.log("Audio downloader. Audio bridge chunk received", { videoId, messageId, audioDownloadType, index: receivedChunks - 1, size: chunk.buffer.byteLength, isLastChunk: chunk.isLastChunk }); notify(); } catch (error) { finish(error instanceof Error ? error : new Error(String(error))); } }; const onAbort = () => finish(makeAbortError(signal.reason)); const streamTimeout = setTimeout(() => finish(/* @__PURE__ */ new Error("Audio bridge stream timed out")), STREAM_TIMEOUT_MS); const navigationInterval = setInterval(() => { if (!globalThis.location.href.includes(videoId)) finish(makeAbortError("URL changed during audio download")); }, 100); globalThis.addEventListener("message", onMessage); signal.addEventListener("abort", onAbort, { once: true }); if (signal.aborted) onAbort(); resetMessageTimeout(); debug.log("Audio downloader. Audio bridge request started", { videoId, sourceLanguage, messageId, audioDownloadType }); try { if (!streamFinished && !failure) globalThis.postMessage({ messageId, messageType: MESSAGE_TYPE$1, messageDirection: "request", payload: { pureVideoId: videoId, audioDownloadType, sourceLanguage } }, "*"); while (!streamFinished || chunks.length > 0) { throwIfFailed(); const chunk = chunks.shift(); if (chunk) yield chunk; else await new Promise((resolve) => { wake = resolve; }); } throwIfFailed(); } finally { clearTimeout(messageTimeout); clearTimeout(streamTimeout); clearInterval(navigationInterval); globalThis.removeEventListener("message", onMessage); signal.removeEventListener("abort", onAbort); if (!streamFinished || failure) postAbort(); } } async function getAudioFromBridge({ videoId, signal, sourceLanguage }, audioDownloadType) { return { fileId: `random-${audioDownloadType}-${crypto.randomUUID()}`, mediaPartsLength: null, getMediaBuffers: () => getAudioBridgeChunks(videoId, signal, audioDownloadType, sourceLanguage) }; } //#endregion //#region src/audioDownloader/strategies/audioChunks.ts function concatBuffers(buffers) { const result = new Uint8Array(buffers.reduce((length, buffer) => length + buffer.byteLength, 0)); let offset = 0; for (const buffer of buffers) { result.set(buffer, offset); offset += buffer.byteLength; } return result; } //#endregion //#region node_modules/googlevideo/dist/protos/generated/misc/common.js function createBaseFormatId() { return { itag: 0, lastModified: "0", xtags: "" }; } var FormatId = { encode(message, writer = new BinaryWriter()) { if (message.itag !== void 0 && message.itag !== 0) writer.uint32(8).int32(message.itag); if (message.lastModified !== void 0 && message.lastModified !== "0") writer.uint32(16).uint64(message.lastModified); if (message.xtags !== void 0 && message.xtags !== "") writer.uint32(26).string(message.xtags); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseFormatId(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.itag = reader.int32(); continue; case 2: if (tag !== 16) break; message.lastModified = reader.uint64().toString(); continue; case 3: if (tag !== 26) break; message.xtags = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseRange() { return { legacyStart: 0, legacyEnd: 0, start: 0, end: 0 }; } var Range = { encode(message, writer = new BinaryWriter()) { if (message.legacyStart !== void 0 && message.legacyStart !== 0) writer.uint32(8).int32(message.legacyStart); if (message.legacyEnd !== void 0 && message.legacyEnd !== 0) writer.uint32(16).int32(message.legacyEnd); if (message.start !== void 0 && message.start !== 0) writer.uint32(24).int32(message.start); if (message.end !== void 0 && message.end !== 0) writer.uint32(32).int32(message.end); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseRange(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.legacyStart = reader.int32(); continue; case 2: if (tag !== 16) break; message.legacyEnd = reader.int32(); continue; case 3: if (tag !== 24) break; message.start = reader.int32(); continue; case 4: if (tag !== 32) break; message.end = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseAuthorizedFormat() { return { trackType: 0, isHdr: false }; } var AuthorizedFormat = { encode(message, writer = new BinaryWriter()) { if (message.trackType !== void 0 && message.trackType !== 0) writer.uint32(8).int32(message.trackType); if (message.isHdr !== void 0 && message.isHdr !== false) writer.uint32(16).bool(message.isHdr); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseAuthorizedFormat(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.trackType = reader.int32(); continue; case 2: if (tag !== 16) break; message.isHdr = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBasePlaybackAuthorization() { return { authorizedFormats: [], sabrLicenseConstraint: /* @__PURE__ */ new Uint8Array(0) }; } var PlaybackAuthorization = { encode(message, writer = new BinaryWriter()) { for (const v of message.authorizedFormats) AuthorizedFormat.encode(v, writer.uint32(10).fork()).join(); if (message.sabrLicenseConstraint !== void 0 && message.sabrLicenseConstraint.length !== 0) writer.uint32(18).bytes(message.sabrLicenseConstraint); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBasePlaybackAuthorization(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.authorizedFormats.push(AuthorizedFormat.decode(reader, reader.uint32())); continue; case 2: if (tag !== 18) break; message.sabrLicenseConstraint = reader.bytes(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/format_initialization_metadata.js function createBaseFormatInitializationMetadata() { return { videoId: "", formatId: void 0, endTimeMs: "0", endSegmentNumber: "0", mimeType: "", initRange: void 0, indexRange: void 0, field8: "0", durationUnits: "0", durationTimescale: "0" }; } var FormatInitializationMetadata = { encode(message, writer = new BinaryWriter()) { if (message.videoId !== void 0 && message.videoId !== "") writer.uint32(10).string(message.videoId); if (message.formatId !== void 0) FormatId.encode(message.formatId, writer.uint32(18).fork()).join(); if (message.endTimeMs !== void 0 && message.endTimeMs !== "0") writer.uint32(24).int64(message.endTimeMs); if (message.endSegmentNumber !== void 0 && message.endSegmentNumber !== "0") writer.uint32(32).int64(message.endSegmentNumber); if (message.mimeType !== void 0 && message.mimeType !== "") writer.uint32(42).string(message.mimeType); if (message.initRange !== void 0) Range.encode(message.initRange, writer.uint32(50).fork()).join(); if (message.indexRange !== void 0) Range.encode(message.indexRange, writer.uint32(58).fork()).join(); if (message.field8 !== void 0 && message.field8 !== "0") writer.uint32(64).int64(message.field8); if (message.durationUnits !== void 0 && message.durationUnits !== "0") writer.uint32(72).int64(message.durationUnits); if (message.durationTimescale !== void 0 && message.durationTimescale !== "0") writer.uint32(80).int64(message.durationTimescale); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseFormatInitializationMetadata(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.videoId = reader.string(); continue; case 2: if (tag !== 18) break; message.formatId = FormatId.decode(reader, reader.uint32()); continue; case 3: if (tag !== 24) break; message.endTimeMs = reader.int64().toString(); continue; case 4: if (tag !== 32) break; message.endSegmentNumber = reader.int64().toString(); continue; case 5: if (tag !== 42) break; message.mimeType = reader.string(); continue; case 6: if (tag !== 50) break; message.initRange = Range.decode(reader, reader.uint32()); continue; case 7: if (tag !== 58) break; message.indexRange = Range.decode(reader, reader.uint32()); continue; case 8: if (tag !== 64) break; message.field8 = reader.int64().toString(); continue; case 9: if (tag !== 72) break; message.durationUnits = reader.int64().toString(); continue; case 10: if (tag !== 80) break; message.durationTimescale = reader.int64().toString(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/time_range.js function createBaseTimeRange() { return { startTicks: "0", durationTicks: "0", timescale: 0 }; } var TimeRange = { encode(message, writer = new BinaryWriter()) { if (message.startTicks !== void 0 && message.startTicks !== "0") writer.uint32(8).int64(message.startTicks); if (message.durationTicks !== void 0 && message.durationTicks !== "0") writer.uint32(16).int64(message.durationTicks); if (message.timescale !== void 0 && message.timescale !== 0) writer.uint32(24).int32(message.timescale); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseTimeRange(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.startTicks = reader.int64().toString(); continue; case 2: if (tag !== 16) break; message.durationTicks = reader.int64().toString(); continue; case 3: if (tag !== 24) break; message.timescale = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/media_header.js function createBaseMediaHeader() { return { headerId: 0, videoId: "", itag: 0, lmt: "0", xtags: "", startRange: "0", compressionAlgorithm: 0, isInitSeg: false, sequenceNumber: 0, bitrateBps: "0", startMs: "0", durationMs: "0", formatId: void 0, contentLength: "0", timeRange: void 0, sequenceLmt: "0" }; } var MediaHeader = { encode(message, writer = new BinaryWriter()) { if (message.headerId !== void 0 && message.headerId !== 0) writer.uint32(8).uint32(message.headerId); if (message.videoId !== void 0 && message.videoId !== "") writer.uint32(18).string(message.videoId); if (message.itag !== void 0 && message.itag !== 0) writer.uint32(24).int32(message.itag); if (message.lmt !== void 0 && message.lmt !== "0") writer.uint32(32).uint64(message.lmt); if (message.xtags !== void 0 && message.xtags !== "") writer.uint32(42).string(message.xtags); if (message.startRange !== void 0 && message.startRange !== "0") writer.uint32(48).int64(message.startRange); if (message.compressionAlgorithm !== void 0 && message.compressionAlgorithm !== 0) writer.uint32(56).int32(message.compressionAlgorithm); if (message.isInitSeg !== void 0 && message.isInitSeg !== false) writer.uint32(64).bool(message.isInitSeg); if (message.sequenceNumber !== void 0 && message.sequenceNumber !== 0) writer.uint32(72).int32(message.sequenceNumber); if (message.bitrateBps !== void 0 && message.bitrateBps !== "0") writer.uint32(80).int64(message.bitrateBps); if (message.startMs !== void 0 && message.startMs !== "0") writer.uint32(88).int64(message.startMs); if (message.durationMs !== void 0 && message.durationMs !== "0") writer.uint32(96).int64(message.durationMs); if (message.formatId !== void 0) FormatId.encode(message.formatId, writer.uint32(106).fork()).join(); if (message.contentLength !== void 0 && message.contentLength !== "0") writer.uint32(112).int64(message.contentLength); if (message.timeRange !== void 0) TimeRange.encode(message.timeRange, writer.uint32(122).fork()).join(); if (message.sequenceLmt !== void 0 && message.sequenceLmt !== "0") writer.uint32(128).uint64(message.sequenceLmt); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseMediaHeader(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.headerId = reader.uint32(); continue; case 2: if (tag !== 18) break; message.videoId = reader.string(); continue; case 3: if (tag !== 24) break; message.itag = reader.int32(); continue; case 4: if (tag !== 32) break; message.lmt = reader.uint64().toString(); continue; case 5: if (tag !== 42) break; message.xtags = reader.string(); continue; case 6: if (tag !== 48) break; message.startRange = reader.int64().toString(); continue; case 7: if (tag !== 56) break; message.compressionAlgorithm = reader.int32(); continue; case 8: if (tag !== 64) break; message.isInitSeg = reader.bool(); continue; case 9: if (tag !== 72) break; message.sequenceNumber = reader.int32(); continue; case 10: if (tag !== 80) break; message.bitrateBps = reader.int64().toString(); continue; case 11: if (tag !== 88) break; message.startMs = reader.int64().toString(); continue; case 12: if (tag !== 96) break; message.durationMs = reader.int64().toString(); continue; case 13: if (tag !== 106) break; message.formatId = FormatId.decode(reader, reader.uint32()); continue; case 14: if (tag !== 112) break; message.contentLength = reader.int64().toString(); continue; case 15: if (tag !== 122) break; message.timeRange = TimeRange.decode(reader, reader.uint32()); continue; case 16: if (tag !== 128) break; message.sequenceLmt = reader.uint64().toString(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/buffered_range.js function createBaseBufferedRange() { return { formatId: void 0, startTimeMs: "0", durationMs: "0", startSegmentIndex: 0, endSegmentIndex: 0, timeRange: void 0, field9: void 0, field11: void 0, field12: void 0 }; } var BufferedRange = { encode(message, writer = new BinaryWriter()) { if (message.formatId !== void 0) FormatId.encode(message.formatId, writer.uint32(10).fork()).join(); if (message.startTimeMs !== "0") writer.uint32(16).int64(message.startTimeMs); if (message.durationMs !== "0") writer.uint32(24).int64(message.durationMs); if (message.startSegmentIndex !== 0) writer.uint32(32).int32(message.startSegmentIndex); if (message.endSegmentIndex !== 0) writer.uint32(40).int32(message.endSegmentIndex); if (message.timeRange !== void 0) TimeRange.encode(message.timeRange, writer.uint32(50).fork()).join(); if (message.field9 !== void 0) BufferedRange_UnknownMessage1.encode(message.field9, writer.uint32(74).fork()).join(); if (message.field11 !== void 0) BufferedRange_UnknownMessage2.encode(message.field11, writer.uint32(90).fork()).join(); if (message.field12 !== void 0) BufferedRange_UnknownMessage2.encode(message.field12, writer.uint32(98).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseBufferedRange(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.formatId = FormatId.decode(reader, reader.uint32()); continue; case 2: if (tag !== 16) break; message.startTimeMs = reader.int64().toString(); continue; case 3: if (tag !== 24) break; message.durationMs = reader.int64().toString(); continue; case 4: if (tag !== 32) break; message.startSegmentIndex = reader.int32(); continue; case 5: if (tag !== 40) break; message.endSegmentIndex = reader.int32(); continue; case 6: if (tag !== 50) break; message.timeRange = TimeRange.decode(reader, reader.uint32()); continue; case 9: if (tag !== 74) break; message.field9 = BufferedRange_UnknownMessage1.decode(reader, reader.uint32()); continue; case 11: if (tag !== 90) break; message.field11 = BufferedRange_UnknownMessage2.decode(reader, reader.uint32()); continue; case 12: if (tag !== 98) break; message.field12 = BufferedRange_UnknownMessage2.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseBufferedRange_UnknownMessage1() { return { field1: [] }; } var BufferedRange_UnknownMessage1 = { encode(message, writer = new BinaryWriter()) { for (const v of message.field1) BufferedRange_UnknownMessage1_UnknownInnerMessage.encode(v, writer.uint32(10).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseBufferedRange_UnknownMessage1(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.field1.push(BufferedRange_UnknownMessage1_UnknownInnerMessage.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseBufferedRange_UnknownMessage1_UnknownInnerMessage() { return { videoId: "", lmt: "0" }; } var BufferedRange_UnknownMessage1_UnknownInnerMessage = { encode(message, writer = new BinaryWriter()) { if (message.videoId !== void 0 && message.videoId !== "") writer.uint32(10).string(message.videoId); if (message.lmt !== void 0 && message.lmt !== "0") writer.uint32(16).uint64(message.lmt); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseBufferedRange_UnknownMessage1_UnknownInnerMessage(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.videoId = reader.string(); continue; case 2: if (tag !== 16) break; message.lmt = reader.uint64().toString(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseBufferedRange_UnknownMessage2() { return { field1: 0, field2: 0, field3: 0 }; } var BufferedRange_UnknownMessage2 = { encode(message, writer = new BinaryWriter()) { if (message.field1 !== void 0 && message.field1 !== 0) writer.uint32(8).int32(message.field1); if (message.field2 !== void 0 && message.field2 !== 0) writer.uint32(16).int32(message.field2); if (message.field3 !== void 0 && message.field3 !== 0) writer.uint32(24).int32(message.field3); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseBufferedRange_UnknownMessage2(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.field1 = reader.int32(); continue; case 2: if (tag !== 16) break; message.field2 = reader.int32(); continue; case 3: if (tag !== 24) break; message.field3 = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/media_capabilities.js function createBaseMediaCapabilities() { return { videoFormatCapabilities: [], audioFormatCapabilities: [], hdrModeBitmask: 0 }; } var MediaCapabilities = { encode(message, writer = new BinaryWriter()) { for (const v of message.videoFormatCapabilities) MediaCapabilities_VideoFormatCapability.encode(v, writer.uint32(10).fork()).join(); for (const v of message.audioFormatCapabilities) MediaCapabilities_AudioFormatCapability.encode(v, writer.uint32(18).fork()).join(); if (message.hdrModeBitmask !== void 0 && message.hdrModeBitmask !== 0) writer.uint32(40).int32(message.hdrModeBitmask); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseMediaCapabilities(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.videoFormatCapabilities.push(MediaCapabilities_VideoFormatCapability.decode(reader, reader.uint32())); continue; case 2: if (tag !== 18) break; message.audioFormatCapabilities.push(MediaCapabilities_AudioFormatCapability.decode(reader, reader.uint32())); continue; case 5: if (tag !== 40) break; message.hdrModeBitmask = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseMediaCapabilities_VideoFormatCapability() { return { videoCodec: 0, maxHeight: 0, maxWidth: 0, maxFramerate: 0, maxBitrateBps: 0, is10BitSupported: false }; } var MediaCapabilities_VideoFormatCapability = { encode(message, writer = new BinaryWriter()) { if (message.videoCodec !== void 0 && message.videoCodec !== 0) writer.uint32(8).int32(message.videoCodec); if (message.maxHeight !== void 0 && message.maxHeight !== 0) writer.uint32(24).int32(message.maxHeight); if (message.maxWidth !== void 0 && message.maxWidth !== 0) writer.uint32(32).int32(message.maxWidth); if (message.maxFramerate !== void 0 && message.maxFramerate !== 0) writer.uint32(88).int32(message.maxFramerate); if (message.maxBitrateBps !== void 0 && message.maxBitrateBps !== 0) writer.uint32(96).int32(message.maxBitrateBps); if (message.is10BitSupported !== void 0 && message.is10BitSupported !== false) writer.uint32(120).bool(message.is10BitSupported); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseMediaCapabilities_VideoFormatCapability(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.videoCodec = reader.int32(); continue; case 3: if (tag !== 24) break; message.maxHeight = reader.int32(); continue; case 4: if (tag !== 32) break; message.maxWidth = reader.int32(); continue; case 11: if (tag !== 88) break; message.maxFramerate = reader.int32(); continue; case 12: if (tag !== 96) break; message.maxBitrateBps = reader.int32(); continue; case 15: if (tag !== 120) break; message.is10BitSupported = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseMediaCapabilities_AudioFormatCapability() { return { audioCodec: 0, numChannels: 0, maxBitrateBps: 0, spatialCapabilityBitmask: 0 }; } var MediaCapabilities_AudioFormatCapability = { encode(message, writer = new BinaryWriter()) { if (message.audioCodec !== void 0 && message.audioCodec !== 0) writer.uint32(8).int32(message.audioCodec); if (message.numChannels !== void 0 && message.numChannels !== 0) writer.uint32(16).int32(message.numChannels); if (message.maxBitrateBps !== void 0 && message.maxBitrateBps !== 0) writer.uint32(24).int32(message.maxBitrateBps); if (message.spatialCapabilityBitmask !== void 0 && message.spatialCapabilityBitmask !== 0) writer.uint32(48).int32(message.spatialCapabilityBitmask); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseMediaCapabilities_AudioFormatCapability(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.audioCodec = reader.int32(); continue; case 2: if (tag !== 16) break; message.numChannels = reader.int32(); continue; case 3: if (tag !== 24) break; message.maxBitrateBps = reader.int32(); continue; case 6: if (tag !== 48) break; message.spatialCapabilityBitmask = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/playback_cookie.js function createBasePlaybackCookie() { return { resolution: 0, field2: 0, videoFmt: void 0, audioFmt: void 0 }; } var PlaybackCookie = { encode(message, writer = new BinaryWriter()) { if (message.resolution !== void 0 && message.resolution !== 0) writer.uint32(8).int32(message.resolution); if (message.field2 !== void 0 && message.field2 !== 0) writer.uint32(16).int32(message.field2); if (message.videoFmt !== void 0) FormatId.encode(message.videoFmt, writer.uint32(58).fork()).join(); if (message.audioFmt !== void 0) FormatId.encode(message.audioFmt, writer.uint32(66).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBasePlaybackCookie(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.resolution = reader.int32(); continue; case 2: if (tag !== 16) break; message.field2 = reader.int32(); continue; case 7: if (tag !== 58) break; message.videoFmt = FormatId.decode(reader, reader.uint32()); continue; case 8: if (tag !== 66) break; message.audioFmt = FormatId.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/client_abr_state.js function createBaseClientAbrState() { return { timeSinceLastManualFormatSelectionMs: "0", lastManualDirection: 0, lastManualSelectedResolution: 0, detailedNetworkType: 0, clientViewportWidth: 0, clientViewportHeight: 0, clientBitrateCapBytesPerSec: "0", stickyResolution: 0, clientViewportIsFlexible: false, bandwidthEstimate: "0", minAudioQuality: 0, maxAudioQuality: 0, videoQualitySetting: 0, audioRoute: 0, playerTimeMs: "0", timeSinceLastSeek: "0", dataSaverMode: false, networkMeteredState: 0, visibility: 0, playbackRate: 0, elapsedWallTimeMs: "0", mediaCapabilities: void 0, timeSinceLastActionMs: "0", enabledTrackTypesBitfield: 0, maxPacingRate: 0, playerState: "0", drcEnabled: false, field48: 0, field50: 0, field51: 0, sabrReportRequestCancellationInfo: 0, disableStreamingXhr: false, field57: "0", preferVp9: false, av1QualityThreshold: 0, field60: 0, isPrefetch: false, sabrSupportQualityConstraints: false, sabrLicenseConstraint: /* @__PURE__ */ new Uint8Array(0), allowProximaLiveLatency: 0, sabrForceProxima: 0, field67: 0, sabrForceMaxNetworkInterruptionDurationMs: "0", audioTrackId: "", enableVoiceBoost: false, playbackAuthorization: void 0 }; } var ClientAbrState = { encode(message, writer = new BinaryWriter()) { if (message.timeSinceLastManualFormatSelectionMs !== void 0 && message.timeSinceLastManualFormatSelectionMs !== "0") writer.uint32(104).int64(message.timeSinceLastManualFormatSelectionMs); if (message.lastManualDirection !== void 0 && message.lastManualDirection !== 0) writer.uint32(112).sint32(message.lastManualDirection); if (message.lastManualSelectedResolution !== void 0 && message.lastManualSelectedResolution !== 0) writer.uint32(128).int32(message.lastManualSelectedResolution); if (message.detailedNetworkType !== void 0 && message.detailedNetworkType !== 0) writer.uint32(136).int32(message.detailedNetworkType); if (message.clientViewportWidth !== void 0 && message.clientViewportWidth !== 0) writer.uint32(144).int32(message.clientViewportWidth); if (message.clientViewportHeight !== void 0 && message.clientViewportHeight !== 0) writer.uint32(152).int32(message.clientViewportHeight); if (message.clientBitrateCapBytesPerSec !== void 0 && message.clientBitrateCapBytesPerSec !== "0") writer.uint32(160).int64(message.clientBitrateCapBytesPerSec); if (message.stickyResolution !== void 0 && message.stickyResolution !== 0) writer.uint32(168).int32(message.stickyResolution); if (message.clientViewportIsFlexible !== void 0 && message.clientViewportIsFlexible !== false) writer.uint32(176).bool(message.clientViewportIsFlexible); if (message.bandwidthEstimate !== void 0 && message.bandwidthEstimate !== "0") writer.uint32(184).int64(message.bandwidthEstimate); if (message.minAudioQuality !== void 0 && message.minAudioQuality !== 0) writer.uint32(192).int32(message.minAudioQuality); if (message.maxAudioQuality !== void 0 && message.maxAudioQuality !== 0) writer.uint32(200).int32(message.maxAudioQuality); if (message.videoQualitySetting !== void 0 && message.videoQualitySetting !== 0) writer.uint32(208).int32(message.videoQualitySetting); if (message.audioRoute !== void 0 && message.audioRoute !== 0) writer.uint32(216).int32(message.audioRoute); if (message.playerTimeMs !== void 0 && message.playerTimeMs !== "0") writer.uint32(224).int64(message.playerTimeMs); if (message.timeSinceLastSeek !== void 0 && message.timeSinceLastSeek !== "0") writer.uint32(232).int64(message.timeSinceLastSeek); if (message.dataSaverMode !== void 0 && message.dataSaverMode !== false) writer.uint32(240).bool(message.dataSaverMode); if (message.networkMeteredState !== void 0 && message.networkMeteredState !== 0) writer.uint32(256).int32(message.networkMeteredState); if (message.visibility !== void 0 && message.visibility !== 0) writer.uint32(272).int32(message.visibility); if (message.playbackRate !== void 0 && message.playbackRate !== 0) writer.uint32(285).float(message.playbackRate); if (message.elapsedWallTimeMs !== void 0 && message.elapsedWallTimeMs !== "0") writer.uint32(288).int64(message.elapsedWallTimeMs); if (message.mediaCapabilities !== void 0) MediaCapabilities.encode(message.mediaCapabilities, writer.uint32(306).fork()).join(); if (message.timeSinceLastActionMs !== void 0 && message.timeSinceLastActionMs !== "0") writer.uint32(312).int64(message.timeSinceLastActionMs); if (message.enabledTrackTypesBitfield !== void 0 && message.enabledTrackTypesBitfield !== 0) writer.uint32(320).int32(message.enabledTrackTypesBitfield); if (message.maxPacingRate !== void 0 && message.maxPacingRate !== 0) writer.uint32(344).int32(message.maxPacingRate); if (message.playerState !== void 0 && message.playerState !== "0") writer.uint32(352).int64(message.playerState); if (message.drcEnabled !== void 0 && message.drcEnabled !== false) writer.uint32(368).bool(message.drcEnabled); if (message.field48 !== void 0 && message.field48 !== 0) writer.uint32(384).int32(message.field48); if (message.field50 !== void 0 && message.field50 !== 0) writer.uint32(400).int32(message.field50); if (message.field51 !== void 0 && message.field51 !== 0) writer.uint32(408).int32(message.field51); if (message.sabrReportRequestCancellationInfo !== void 0 && message.sabrReportRequestCancellationInfo !== 0) writer.uint32(432).int32(message.sabrReportRequestCancellationInfo); if (message.disableStreamingXhr !== void 0 && message.disableStreamingXhr !== false) writer.uint32(448).bool(message.disableStreamingXhr); if (message.field57 !== void 0 && message.field57 !== "0") writer.uint32(456).int64(message.field57); if (message.preferVp9 !== void 0 && message.preferVp9 !== false) writer.uint32(464).bool(message.preferVp9); if (message.av1QualityThreshold !== void 0 && message.av1QualityThreshold !== 0) writer.uint32(472).int32(message.av1QualityThreshold); if (message.field60 !== void 0 && message.field60 !== 0) writer.uint32(480).int32(message.field60); if (message.isPrefetch !== void 0 && message.isPrefetch !== false) writer.uint32(488).bool(message.isPrefetch); if (message.sabrSupportQualityConstraints !== void 0 && message.sabrSupportQualityConstraints !== false) writer.uint32(496).bool(message.sabrSupportQualityConstraints); if (message.sabrLicenseConstraint !== void 0 && message.sabrLicenseConstraint.length !== 0) writer.uint32(506).bytes(message.sabrLicenseConstraint); if (message.allowProximaLiveLatency !== void 0 && message.allowProximaLiveLatency !== 0) writer.uint32(512).int32(message.allowProximaLiveLatency); if (message.sabrForceProxima !== void 0 && message.sabrForceProxima !== 0) writer.uint32(528).int32(message.sabrForceProxima); if (message.field67 !== void 0 && message.field67 !== 0) writer.uint32(536).int32(message.field67); if (message.sabrForceMaxNetworkInterruptionDurationMs !== void 0 && message.sabrForceMaxNetworkInterruptionDurationMs !== "0") writer.uint32(544).int64(message.sabrForceMaxNetworkInterruptionDurationMs); if (message.audioTrackId !== void 0 && message.audioTrackId !== "") writer.uint32(554).string(message.audioTrackId); if (message.enableVoiceBoost !== void 0 && message.enableVoiceBoost !== false) writer.uint32(608).bool(message.enableVoiceBoost); if (message.playbackAuthorization !== void 0) PlaybackAuthorization.encode(message.playbackAuthorization, writer.uint32(634).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseClientAbrState(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 13: if (tag !== 104) break; message.timeSinceLastManualFormatSelectionMs = reader.int64().toString(); continue; case 14: if (tag !== 112) break; message.lastManualDirection = reader.sint32(); continue; case 16: if (tag !== 128) break; message.lastManualSelectedResolution = reader.int32(); continue; case 17: if (tag !== 136) break; message.detailedNetworkType = reader.int32(); continue; case 18: if (tag !== 144) break; message.clientViewportWidth = reader.int32(); continue; case 19: if (tag !== 152) break; message.clientViewportHeight = reader.int32(); continue; case 20: if (tag !== 160) break; message.clientBitrateCapBytesPerSec = reader.int64().toString(); continue; case 21: if (tag !== 168) break; message.stickyResolution = reader.int32(); continue; case 22: if (tag !== 176) break; message.clientViewportIsFlexible = reader.bool(); continue; case 23: if (tag !== 184) break; message.bandwidthEstimate = reader.int64().toString(); continue; case 24: if (tag !== 192) break; message.minAudioQuality = reader.int32(); continue; case 25: if (tag !== 200) break; message.maxAudioQuality = reader.int32(); continue; case 26: if (tag !== 208) break; message.videoQualitySetting = reader.int32(); continue; case 27: if (tag !== 216) break; message.audioRoute = reader.int32(); continue; case 28: if (tag !== 224) break; message.playerTimeMs = reader.int64().toString(); continue; case 29: if (tag !== 232) break; message.timeSinceLastSeek = reader.int64().toString(); continue; case 30: if (tag !== 240) break; message.dataSaverMode = reader.bool(); continue; case 32: if (tag !== 256) break; message.networkMeteredState = reader.int32(); continue; case 34: if (tag !== 272) break; message.visibility = reader.int32(); continue; case 35: if (tag !== 285) break; message.playbackRate = reader.float(); continue; case 36: if (tag !== 288) break; message.elapsedWallTimeMs = reader.int64().toString(); continue; case 38: if (tag !== 306) break; message.mediaCapabilities = MediaCapabilities.decode(reader, reader.uint32()); continue; case 39: if (tag !== 312) break; message.timeSinceLastActionMs = reader.int64().toString(); continue; case 40: if (tag !== 320) break; message.enabledTrackTypesBitfield = reader.int32(); continue; case 43: if (tag !== 344) break; message.maxPacingRate = reader.int32(); continue; case 44: if (tag !== 352) break; message.playerState = reader.int64().toString(); continue; case 46: if (tag !== 368) break; message.drcEnabled = reader.bool(); continue; case 48: if (tag !== 384) break; message.field48 = reader.int32(); continue; case 50: if (tag !== 400) break; message.field50 = reader.int32(); continue; case 51: if (tag !== 408) break; message.field51 = reader.int32(); continue; case 54: if (tag !== 432) break; message.sabrReportRequestCancellationInfo = reader.int32(); continue; case 56: if (tag !== 448) break; message.disableStreamingXhr = reader.bool(); continue; case 57: if (tag !== 456) break; message.field57 = reader.int64().toString(); continue; case 58: if (tag !== 464) break; message.preferVp9 = reader.bool(); continue; case 59: if (tag !== 472) break; message.av1QualityThreshold = reader.int32(); continue; case 60: if (tag !== 480) break; message.field60 = reader.int32(); continue; case 61: if (tag !== 488) break; message.isPrefetch = reader.bool(); continue; case 62: if (tag !== 496) break; message.sabrSupportQualityConstraints = reader.bool(); continue; case 63: if (tag !== 506) break; message.sabrLicenseConstraint = reader.bytes(); continue; case 64: if (tag !== 512) break; message.allowProximaLiveLatency = reader.int32(); continue; case 66: if (tag !== 528) break; message.sabrForceProxima = reader.int32(); continue; case 67: if (tag !== 536) break; message.field67 = reader.int32(); continue; case 68: if (tag !== 544) break; message.sabrForceMaxNetworkInterruptionDurationMs = reader.int64().toString(); continue; case 69: if (tag !== 554) break; message.audioTrackId = reader.string(); continue; case 76: if (tag !== 608) break; message.enableVoiceBoost = reader.bool(); continue; case 79: if (tag !== 634) break; message.playbackAuthorization = PlaybackAuthorization.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/streamer_context.js function createBaseStreamerContext() { return { clientInfo: void 0, poToken: /* @__PURE__ */ new Uint8Array(0), playbackCookie: /* @__PURE__ */ new Uint8Array(0), field4: /* @__PURE__ */ new Uint8Array(0), sabrContexts: [], unsentSabrContexts: [], field7: "", field8: void 0 }; } var StreamerContext = { encode(message, writer = new BinaryWriter()) { if (message.clientInfo !== void 0) StreamerContext_ClientInfo.encode(message.clientInfo, writer.uint32(10).fork()).join(); if (message.poToken !== void 0 && message.poToken.length !== 0) writer.uint32(18).bytes(message.poToken); if (message.playbackCookie !== void 0 && message.playbackCookie.length !== 0) writer.uint32(26).bytes(message.playbackCookie); if (message.field4 !== void 0 && message.field4.length !== 0) writer.uint32(34).bytes(message.field4); for (const v of message.sabrContexts) StreamerContext_SabrContext.encode(v, writer.uint32(42).fork()).join(); writer.uint32(50).fork(); for (const v of message.unsentSabrContexts) writer.int32(v); writer.join(); if (message.field7 !== void 0 && message.field7 !== "") writer.uint32(58).string(message.field7); if (message.field8 !== void 0) StreamerContext_UnknownMessage1.encode(message.field8, writer.uint32(66).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamerContext(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.clientInfo = StreamerContext_ClientInfo.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) break; message.poToken = reader.bytes(); continue; case 3: if (tag !== 26) break; message.playbackCookie = reader.bytes(); continue; case 4: if (tag !== 34) break; message.field4 = reader.bytes(); continue; case 5: if (tag !== 42) break; message.sabrContexts.push(StreamerContext_SabrContext.decode(reader, reader.uint32())); continue; case 6: if (tag === 48) { message.unsentSabrContexts.push(reader.int32()); continue; } if (tag === 50) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) message.unsentSabrContexts.push(reader.int32()); continue; } break; case 7: if (tag !== 58) break; message.field7 = reader.string(); continue; case 8: if (tag !== 66) break; message.field8 = StreamerContext_UnknownMessage1.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseStreamerContext_ClientInfo() { return { deviceMake: "", deviceModel: "", clientName: 0, clientVersion: "", osName: "", osVersion: "", acceptLanguage: "", acceptRegion: "", screenWidthPoints: 0, screenHeightPoints: 0, screenWidthInches: 0, screenHeightInches: 0, screenPixelDensity: 0, clientFormFactor: 0, gmscoreVersionCode: 0, windowWidthPoints: 0, windowHeightPoints: 0, androidSdkVersion: 0, screenDensityFloat: 0, utcOffsetMinutes: "0", timeZone: "", chipset: "", glDeviceInfo: void 0 }; } var StreamerContext_ClientInfo = { encode(message, writer = new BinaryWriter()) { if (message.deviceMake !== void 0 && message.deviceMake !== "") writer.uint32(98).string(message.deviceMake); if (message.deviceModel !== void 0 && message.deviceModel !== "") writer.uint32(106).string(message.deviceModel); if (message.clientName !== void 0 && message.clientName !== 0) writer.uint32(128).int32(message.clientName); if (message.clientVersion !== void 0 && message.clientVersion !== "") writer.uint32(138).string(message.clientVersion); if (message.osName !== void 0 && message.osName !== "") writer.uint32(146).string(message.osName); if (message.osVersion !== void 0 && message.osVersion !== "") writer.uint32(154).string(message.osVersion); if (message.acceptLanguage !== void 0 && message.acceptLanguage !== "") writer.uint32(170).string(message.acceptLanguage); if (message.acceptRegion !== void 0 && message.acceptRegion !== "") writer.uint32(178).string(message.acceptRegion); if (message.screenWidthPoints !== void 0 && message.screenWidthPoints !== 0) writer.uint32(296).int32(message.screenWidthPoints); if (message.screenHeightPoints !== void 0 && message.screenHeightPoints !== 0) writer.uint32(304).int32(message.screenHeightPoints); if (message.screenWidthInches !== void 0 && message.screenWidthInches !== 0) writer.uint32(317).float(message.screenWidthInches); if (message.screenHeightInches !== void 0 && message.screenHeightInches !== 0) writer.uint32(325).float(message.screenHeightInches); if (message.screenPixelDensity !== void 0 && message.screenPixelDensity !== 0) writer.uint32(328).int32(message.screenPixelDensity); if (message.clientFormFactor !== void 0 && message.clientFormFactor !== 0) writer.uint32(368).int32(message.clientFormFactor); if (message.gmscoreVersionCode !== void 0 && message.gmscoreVersionCode !== 0) writer.uint32(400).int32(message.gmscoreVersionCode); if (message.windowWidthPoints !== void 0 && message.windowWidthPoints !== 0) writer.uint32(440).int32(message.windowWidthPoints); if (message.windowHeightPoints !== void 0 && message.windowHeightPoints !== 0) writer.uint32(448).int32(message.windowHeightPoints); if (message.androidSdkVersion !== void 0 && message.androidSdkVersion !== 0) writer.uint32(512).int32(message.androidSdkVersion); if (message.screenDensityFloat !== void 0 && message.screenDensityFloat !== 0) writer.uint32(525).float(message.screenDensityFloat); if (message.utcOffsetMinutes !== void 0 && message.utcOffsetMinutes !== "0") writer.uint32(536).int64(message.utcOffsetMinutes); if (message.timeZone !== void 0 && message.timeZone !== "") writer.uint32(642).string(message.timeZone); if (message.chipset !== void 0 && message.chipset !== "") writer.uint32(738).string(message.chipset); if (message.glDeviceInfo !== void 0) StreamerContext_GLDeviceInfo.encode(message.glDeviceInfo, writer.uint32(818).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamerContext_ClientInfo(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 12: if (tag !== 98) break; message.deviceMake = reader.string(); continue; case 13: if (tag !== 106) break; message.deviceModel = reader.string(); continue; case 16: if (tag !== 128) break; message.clientName = reader.int32(); continue; case 17: if (tag !== 138) break; message.clientVersion = reader.string(); continue; case 18: if (tag !== 146) break; message.osName = reader.string(); continue; case 19: if (tag !== 154) break; message.osVersion = reader.string(); continue; case 21: if (tag !== 170) break; message.acceptLanguage = reader.string(); continue; case 22: if (tag !== 178) break; message.acceptRegion = reader.string(); continue; case 37: if (tag !== 296) break; message.screenWidthPoints = reader.int32(); continue; case 38: if (tag !== 304) break; message.screenHeightPoints = reader.int32(); continue; case 39: if (tag !== 317) break; message.screenWidthInches = reader.float(); continue; case 40: if (tag !== 325) break; message.screenHeightInches = reader.float(); continue; case 41: if (tag !== 328) break; message.screenPixelDensity = reader.int32(); continue; case 46: if (tag !== 368) break; message.clientFormFactor = reader.int32(); continue; case 50: if (tag !== 400) break; message.gmscoreVersionCode = reader.int32(); continue; case 55: if (tag !== 440) break; message.windowWidthPoints = reader.int32(); continue; case 56: if (tag !== 448) break; message.windowHeightPoints = reader.int32(); continue; case 64: if (tag !== 512) break; message.androidSdkVersion = reader.int32(); continue; case 65: if (tag !== 525) break; message.screenDensityFloat = reader.float(); continue; case 67: if (tag !== 536) break; message.utcOffsetMinutes = reader.int64().toString(); continue; case 80: if (tag !== 642) break; message.timeZone = reader.string(); continue; case 92: if (tag !== 738) break; message.chipset = reader.string(); continue; case 102: if (tag !== 818) break; message.glDeviceInfo = StreamerContext_GLDeviceInfo.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseStreamerContext_GLDeviceInfo() { return { glRenderer: "", glEsVersionMajor: 0, glEsVersionMinor: 0 }; } var StreamerContext_GLDeviceInfo = { encode(message, writer = new BinaryWriter()) { if (message.glRenderer !== void 0 && message.glRenderer !== "") writer.uint32(10).string(message.glRenderer); if (message.glEsVersionMajor !== void 0 && message.glEsVersionMajor !== 0) writer.uint32(16).int32(message.glEsVersionMajor); if (message.glEsVersionMinor !== void 0 && message.glEsVersionMinor !== 0) writer.uint32(24).int32(message.glEsVersionMinor); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamerContext_GLDeviceInfo(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.glRenderer = reader.string(); continue; case 2: if (tag !== 16) break; message.glEsVersionMajor = reader.int32(); continue; case 3: if (tag !== 24) break; message.glEsVersionMinor = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseStreamerContext_SabrContext() { return { type: 0, value: /* @__PURE__ */ new Uint8Array(0) }; } var StreamerContext_SabrContext = { encode(message, writer = new BinaryWriter()) { if (message.type !== void 0 && message.type !== 0) writer.uint32(8).int32(message.type); if (message.value !== void 0 && message.value.length !== 0) writer.uint32(18).bytes(message.value); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamerContext_SabrContext(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.type = reader.int32(); continue; case 2: if (tag !== 18) break; message.value = reader.bytes(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseStreamerContext_UnknownMessage1() { return { field1: /* @__PURE__ */ new Uint8Array(0), field2: void 0 }; } var StreamerContext_UnknownMessage1 = { encode(message, writer = new BinaryWriter()) { if (message.field1 !== void 0 && message.field1.length !== 0) writer.uint32(10).bytes(message.field1); if (message.field2 !== void 0) StreamerContext_UnknownMessage1_UnknownInnerMessage1.encode(message.field2, writer.uint32(18).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamerContext_UnknownMessage1(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.field1 = reader.bytes(); continue; case 2: if (tag !== 18) break; message.field2 = StreamerContext_UnknownMessage1_UnknownInnerMessage1.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseStreamerContext_UnknownMessage1_UnknownInnerMessage1() { return { code: 0, message: "" }; } var StreamerContext_UnknownMessage1_UnknownInnerMessage1 = { encode(message, writer = new BinaryWriter()) { if (message.code !== void 0 && message.code !== 0) writer.uint32(8).int32(message.code); if (message.message !== void 0 && message.message !== "") writer.uint32(18).string(message.message); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamerContext_UnknownMessage1_UnknownInnerMessage1(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.code = reader.int32(); continue; case 2: if (tag !== 18) break; message.message = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/video_playback_abr_request.js function createBaseVideoPlaybackAbrRequest() { return { clientAbrState: void 0, selectedFormatIds: [], bufferedRanges: [], playerTimeMs: "0", videoPlaybackUstreamerConfig: /* @__PURE__ */ new Uint8Array(0), field6: void 0, preferredAudioFormatIds: [], preferredVideoFormatIds: [], preferredSubtitleFormatIds: [], streamerContext: void 0, field21: void 0, field22: 0, field23: 0, field1000: [] }; } var VideoPlaybackAbrRequest = { encode(message, writer = new BinaryWriter()) { if (message.clientAbrState !== void 0) ClientAbrState.encode(message.clientAbrState, writer.uint32(10).fork()).join(); for (const v of message.selectedFormatIds) FormatId.encode(v, writer.uint32(18).fork()).join(); for (const v of message.bufferedRanges) BufferedRange.encode(v, writer.uint32(26).fork()).join(); if (message.playerTimeMs !== void 0 && message.playerTimeMs !== "0") writer.uint32(32).int64(message.playerTimeMs); if (message.videoPlaybackUstreamerConfig !== void 0 && message.videoPlaybackUstreamerConfig.length !== 0) writer.uint32(42).bytes(message.videoPlaybackUstreamerConfig); if (message.field6 !== void 0) UnknownMessage1.encode(message.field6, writer.uint32(50).fork()).join(); for (const v of message.preferredAudioFormatIds) FormatId.encode(v, writer.uint32(130).fork()).join(); for (const v of message.preferredVideoFormatIds) FormatId.encode(v, writer.uint32(138).fork()).join(); for (const v of message.preferredSubtitleFormatIds) FormatId.encode(v, writer.uint32(146).fork()).join(); if (message.streamerContext !== void 0) StreamerContext.encode(message.streamerContext, writer.uint32(154).fork()).join(); if (message.field21 !== void 0) UnknownMessage2.encode(message.field21, writer.uint32(170).fork()).join(); if (message.field22 !== void 0 && message.field22 !== 0) writer.uint32(176).int32(message.field22); if (message.field23 !== void 0 && message.field23 !== 0) writer.uint32(184).int32(message.field23); for (const v of message.field1000) UnknownMessage3.encode(v, writer.uint32(8002).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseVideoPlaybackAbrRequest(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.clientAbrState = ClientAbrState.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) break; message.selectedFormatIds.push(FormatId.decode(reader, reader.uint32())); continue; case 3: if (tag !== 26) break; message.bufferedRanges.push(BufferedRange.decode(reader, reader.uint32())); continue; case 4: if (tag !== 32) break; message.playerTimeMs = reader.int64().toString(); continue; case 5: if (tag !== 42) break; message.videoPlaybackUstreamerConfig = reader.bytes(); continue; case 6: if (tag !== 50) break; message.field6 = UnknownMessage1.decode(reader, reader.uint32()); continue; case 16: if (tag !== 130) break; message.preferredAudioFormatIds.push(FormatId.decode(reader, reader.uint32())); continue; case 17: if (tag !== 138) break; message.preferredVideoFormatIds.push(FormatId.decode(reader, reader.uint32())); continue; case 18: if (tag !== 146) break; message.preferredSubtitleFormatIds.push(FormatId.decode(reader, reader.uint32())); continue; case 19: if (tag !== 154) break; message.streamerContext = StreamerContext.decode(reader, reader.uint32()); continue; case 21: if (tag !== 170) break; message.field21 = UnknownMessage2.decode(reader, reader.uint32()); continue; case 22: if (tag !== 176) break; message.field22 = reader.int32(); continue; case 23: if (tag !== 184) break; message.field23 = reader.int32(); continue; case 1e3: if (tag !== 8002) break; message.field1000.push(UnknownMessage3.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseUnknownMessage1() { return { formatId: void 0, lmt: "0", sequenceNumber: 0, timeRange: void 0, field5: 0 }; } var UnknownMessage1 = { encode(message, writer = new BinaryWriter()) { if (message.formatId !== void 0) FormatId.encode(message.formatId, writer.uint32(10).fork()).join(); if (message.lmt !== void 0 && message.lmt !== "0") writer.uint32(16).sint64(message.lmt); if (message.sequenceNumber !== void 0 && message.sequenceNumber !== 0) writer.uint32(24).int32(message.sequenceNumber); if (message.timeRange !== void 0) TimeRange.encode(message.timeRange, writer.uint32(34).fork()).join(); if (message.field5 !== void 0 && message.field5 !== 0) writer.uint32(40).int32(message.field5); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseUnknownMessage1(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.formatId = FormatId.decode(reader, reader.uint32()); continue; case 2: if (tag !== 16) break; message.lmt = reader.sint64().toString(); continue; case 3: if (tag !== 24) break; message.sequenceNumber = reader.int32(); continue; case 4: if (tag !== 34) break; message.timeRange = TimeRange.decode(reader, reader.uint32()); continue; case 5: if (tag !== 40) break; message.field5 = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseUnknownMessage2() { return { field1: [], field2: /* @__PURE__ */ new Uint8Array(0), field3: "", field4: 0, field5: 0, field6: "" }; } var UnknownMessage2 = { encode(message, writer = new BinaryWriter()) { for (const v of message.field1) writer.uint32(10).string(v); if (message.field2 !== void 0 && message.field2.length !== 0) writer.uint32(18).bytes(message.field2); if (message.field3 !== void 0 && message.field3 !== "") writer.uint32(26).string(message.field3); if (message.field4 !== void 0 && message.field4 !== 0) writer.uint32(32).int32(message.field4); if (message.field5 !== void 0 && message.field5 !== 0) writer.uint32(40).int32(message.field5); if (message.field6 !== void 0 && message.field6 !== "") writer.uint32(50).string(message.field6); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseUnknownMessage2(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.field1.push(reader.string()); continue; case 2: if (tag !== 18) break; message.field2 = reader.bytes(); continue; case 3: if (tag !== 26) break; message.field3 = reader.string(); continue; case 4: if (tag !== 32) break; message.field4 = reader.int32(); continue; case 5: if (tag !== 40) break; message.field5 = reader.int32(); continue; case 6: if (tag !== 50) break; message.field6 = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseUnknownMessage3() { return { formatIds: [], ud: [], clipId: "" }; } var UnknownMessage3 = { encode(message, writer = new BinaryWriter()) { for (const v of message.formatIds) FormatId.encode(v, writer.uint32(10).fork()).join(); for (const v of message.ud) BufferedRange.encode(v, writer.uint32(18).fork()).join(); if (message.clipId !== void 0 && message.clipId !== "") writer.uint32(26).string(message.clipId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseUnknownMessage3(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.formatIds.push(FormatId.decode(reader, reader.uint32())); continue; case 2: if (tag !== 18) break; message.ud.push(BufferedRange.decode(reader, reader.uint32())); continue; case 3: if (tag !== 26) break; message.clipId = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/next_request_policy.js function createBaseNextRequestPolicy() { return { targetAudioReadaheadMs: 0, targetVideoReadaheadMs: 0, maxTimeSinceLastRequestMs: 0, backoffTimeMs: 0, minAudioReadaheadMs: 0, minVideoReadaheadMs: 0, playbackCookie: void 0, videoId: "" }; } var NextRequestPolicy = { encode(message, writer = new BinaryWriter()) { if (message.targetAudioReadaheadMs !== void 0 && message.targetAudioReadaheadMs !== 0) writer.uint32(8).int32(message.targetAudioReadaheadMs); if (message.targetVideoReadaheadMs !== void 0 && message.targetVideoReadaheadMs !== 0) writer.uint32(16).int32(message.targetVideoReadaheadMs); if (message.maxTimeSinceLastRequestMs !== void 0 && message.maxTimeSinceLastRequestMs !== 0) writer.uint32(24).int32(message.maxTimeSinceLastRequestMs); if (message.backoffTimeMs !== void 0 && message.backoffTimeMs !== 0) writer.uint32(32).int32(message.backoffTimeMs); if (message.minAudioReadaheadMs !== void 0 && message.minAudioReadaheadMs !== 0) writer.uint32(40).int32(message.minAudioReadaheadMs); if (message.minVideoReadaheadMs !== void 0 && message.minVideoReadaheadMs !== 0) writer.uint32(48).int32(message.minVideoReadaheadMs); if (message.playbackCookie !== void 0) PlaybackCookie.encode(message.playbackCookie, writer.uint32(58).fork()).join(); if (message.videoId !== void 0 && message.videoId !== "") writer.uint32(66).string(message.videoId); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseNextRequestPolicy(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.targetAudioReadaheadMs = reader.int32(); continue; case 2: if (tag !== 16) break; message.targetVideoReadaheadMs = reader.int32(); continue; case 3: if (tag !== 24) break; message.maxTimeSinceLastRequestMs = reader.int32(); continue; case 4: if (tag !== 32) break; message.backoffTimeMs = reader.int32(); continue; case 5: if (tag !== 40) break; message.minAudioReadaheadMs = reader.int32(); continue; case 6: if (tag !== 48) break; message.minVideoReadaheadMs = reader.int32(); continue; case 7: if (tag !== 58) break; message.playbackCookie = PlaybackCookie.decode(reader, reader.uint32()); continue; case 8: if (tag !== 66) break; message.videoId = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/sabr_error.js function createBaseSabrError() { return { type: "", code: 0 }; } var SabrError = { encode(message, writer = new BinaryWriter()) { if (message.type !== void 0 && message.type !== "") writer.uint32(10).string(message.type); if (message.code !== void 0 && message.code !== 0) writer.uint32(16).int32(message.code); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSabrError(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.type = reader.string(); continue; case 2: if (tag !== 16) break; message.code = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/sabr_redirect.js function createBaseSabrRedirect() { return { url: "" }; } var SabrRedirect = { encode(message, writer = new BinaryWriter()) { if (message.url !== void 0 && message.url !== "") writer.uint32(10).string(message.url); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSabrRedirect(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.url = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/reload_player_response.js function createBaseReloadPlaybackParams() { return { token: "" }; } var ReloadPlaybackParams = { encode(message, writer = new BinaryWriter()) { if (message.token !== void 0 && message.token !== "") writer.uint32(10).string(message.token); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseReloadPlaybackParams(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.token = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; function createBaseReloadPlaybackContext() { return { reloadPlaybackParams: void 0 }; } var ReloadPlaybackContext = { encode(message, writer = new BinaryWriter()) { if (message.reloadPlaybackParams !== void 0) ReloadPlaybackParams.encode(message.reloadPlaybackParams, writer.uint32(10).fork()).join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseReloadPlaybackContext(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) break; message.reloadPlaybackParams = ReloadPlaybackParams.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/sabr_context_update.js var SabrContextUpdate_SabrContextWritePolicy = { UNSPECIFIED: 0, 0: "UNSPECIFIED", OVERWRITE: 1, 1: "OVERWRITE", KEEP_EXISTING: 2, 2: "KEEP_EXISTING", UNRECOGNIZED: -1, "-1": "UNRECOGNIZED" }; function createBaseSabrContextUpdate() { return { type: 0, scope: 0, value: /* @__PURE__ */ new Uint8Array(0), sendByDefault: false, writePolicy: 0 }; } var SabrContextUpdate = { encode(message, writer = new BinaryWriter()) { if (message.type !== void 0 && message.type !== 0) writer.uint32(8).int32(message.type); if (message.scope !== void 0 && message.scope !== 0) writer.uint32(16).int32(message.scope); if (message.value !== void 0 && message.value.length !== 0) writer.uint32(26).bytes(message.value); if (message.sendByDefault !== void 0 && message.sendByDefault !== false) writer.uint32(32).bool(message.sendByDefault); if (message.writePolicy !== void 0 && message.writePolicy !== 0) writer.uint32(40).int32(message.writePolicy); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSabrContextUpdate(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.type = reader.int32(); continue; case 2: if (tag !== 16) break; message.scope = reader.int32(); continue; case 3: if (tag !== 26) break; message.value = reader.bytes(); continue; case 4: if (tag !== 32) break; message.sendByDefault = reader.bool(); continue; case 5: if (tag !== 40) break; message.writePolicy = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/sabr_context_sending_policy.js function createBaseSabrContextSendingPolicy() { return { startPolicy: [], stopPolicy: [], discardPolicy: [] }; } var SabrContextSendingPolicy = { encode(message, writer = new BinaryWriter()) { writer.uint32(10).fork(); for (const v of message.startPolicy) writer.int32(v); writer.join(); writer.uint32(18).fork(); for (const v of message.stopPolicy) writer.int32(v); writer.join(); writer.uint32(26).fork(); for (const v of message.discardPolicy) writer.int32(v); writer.join(); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseSabrContextSendingPolicy(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag === 8) { message.startPolicy.push(reader.int32()); continue; } if (tag === 10) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) message.startPolicy.push(reader.int32()); continue; } break; case 2: if (tag === 16) { message.stopPolicy.push(reader.int32()); continue; } if (tag === 18) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) message.stopPolicy.push(reader.int32()); continue; } break; case 3: if (tag === 24) { message.discardPolicy.push(reader.int32()); continue; } if (tag === 26) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) message.discardPolicy.push(reader.int32()); continue; } } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/stream_protection_status.js function createBaseStreamProtectionStatus() { return { status: 0, maxRetries: 0 }; } var StreamProtectionStatus = { encode(message, writer = new BinaryWriter()) { if (message.status !== void 0 && message.status !== 0) writer.uint32(8).int32(message.status); if (message.maxRetries !== void 0 && message.maxRetries !== 0) writer.uint32(16).int32(message.maxRetries); return writer; }, decode(input, length) { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const end = length === void 0 ? reader.len : reader.pos + length; const message = createBaseStreamProtectionStatus(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) break; message.status = reader.int32(); continue; case 2: if (tag !== 16) break; message.maxRetries = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) break; reader.skip(tag & 7); } return message; } }; //#endregion //#region node_modules/googlevideo/dist/protos/generated/video_streaming/ump_part_id.js var UMPPartId = { UNKNOWN: 0, 0: "UNKNOWN", ONESIE_HEADER: 10, 10: "ONESIE_HEADER", ONESIE_DATA: 11, 11: "ONESIE_DATA", ONESIE_ENCRYPTED_MEDIA: 12, 12: "ONESIE_ENCRYPTED_MEDIA", /** MEDIA_HEADER - Header for a media segment; includes sequence and timing information. */ MEDIA_HEADER: 20, /** MEDIA_HEADER - Header for a media segment; includes sequence and timing information. */ 20: "MEDIA_HEADER", /** MEDIA - Chunk of media segment data. */ MEDIA: 21, /** MEDIA - Chunk of media segment data. */ 21: "MEDIA", /** MEDIA_END - Indicates end of media segment; finalizes segment processing. */ MEDIA_END: 22, /** MEDIA_END - Indicates end of media segment; finalizes segment processing. */ 22: "MEDIA_END", CONFIG: 30, 30: "CONFIG", LIVE_METADATA: 31, 31: "LIVE_METADATA", HOSTNAME_CHANGE_HINT_DEPRECATED: 32, 32: "HOSTNAME_CHANGE_HINT_DEPRECATED", LIVE_METADATA_PROMISE: 33, 33: "LIVE_METADATA_PROMISE", LIVE_METADATA_PROMISE_CANCELLATION: 34, 34: "LIVE_METADATA_PROMISE_CANCELLATION", /** NEXT_REQUEST_POLICY - Server's policy for the next request; includes backoff time and playback cookie. */ NEXT_REQUEST_POLICY: 35, /** NEXT_REQUEST_POLICY - Server's policy for the next request; includes backoff time and playback cookie. */ 35: "NEXT_REQUEST_POLICY", USTREAMER_VIDEO_AND_FORMAT_METADATA: 36, 36: "USTREAMER_VIDEO_AND_FORMAT_METADATA", FORMAT_SELECTION_CONFIG: 37, 37: "FORMAT_SELECTION_CONFIG", USTREAMER_SELECTED_MEDIA_STREAM: 38, 38: "USTREAMER_SELECTED_MEDIA_STREAM", /** FORMAT_INITIALIZATION_METADATA - Metadata for format initialization; contains total number of segments, duration, etc. */ FORMAT_INITIALIZATION_METADATA: 42, /** FORMAT_INITIALIZATION_METADATA - Metadata for format initialization; contains total number of segments, duration, etc. */ 42: "FORMAT_INITIALIZATION_METADATA", /** SABR_REDIRECT - Indicates a redirect to a different streaming URL. */ SABR_REDIRECT: 43, /** SABR_REDIRECT - Indicates a redirect to a different streaming URL. */ 43: "SABR_REDIRECT", /** SABR_ERROR - Indicates a SABR error; happens when the payload is invalid or the server cannot process the request. */ SABR_ERROR: 44, /** SABR_ERROR - Indicates a SABR error; happens when the payload is invalid or the server cannot process the request. */ 44: "SABR_ERROR", SABR_SEEK: 45, 45: "SABR_SEEK", /** RELOAD_PLAYER_RESPONSE - Directive to reload the player with new parameters. */ RELOAD_PLAYER_RESPONSE: 46, /** RELOAD_PLAYER_RESPONSE - Directive to reload the player with new parameters. */ 46: "RELOAD_PLAYER_RESPONSE", PLAYBACK_START_POLICY: 47, 47: "PLAYBACK_START_POLICY", ALLOWED_CACHED_FORMATS: 48, 48: "ALLOWED_CACHED_FORMATS", START_BW_SAMPLING_HINT: 49, 49: "START_BW_SAMPLING_HINT", PAUSE_BW_SAMPLING_HINT: 50, 50: "PAUSE_BW_SAMPLING_HINT", SELECTABLE_FORMATS: 51, 51: "SELECTABLE_FORMATS", REQUEST_IDENTIFIER: 52, 52: "REQUEST_IDENTIFIER", REQUEST_CANCELLATION_POLICY: 53, 53: "REQUEST_CANCELLATION_POLICY", ONESIE_PREFETCH_REJECTION: 54, 54: "ONESIE_PREFETCH_REJECTION", TIMELINE_CONTEXT: 55, 55: "TIMELINE_CONTEXT", REQUEST_PIPELINING: 56, 56: "REQUEST_PIPELINING", /** SABR_CONTEXT_UPDATE - Updates SABR context data; usually used for ads. */ SABR_CONTEXT_UPDATE: 57, /** SABR_CONTEXT_UPDATE - Updates SABR context data; usually used for ads. */ 57: "SABR_CONTEXT_UPDATE", /** STREAM_PROTECTION_STATUS - Status of stream protection; indicates whether attestation is required. */ STREAM_PROTECTION_STATUS: 58, /** STREAM_PROTECTION_STATUS - Status of stream protection; indicates whether attestation is required. */ 58: "STREAM_PROTECTION_STATUS", /** SABR_CONTEXT_SENDING_POLICY - Policy indicating which SABR contexts to send or discard in future requests. */ SABR_CONTEXT_SENDING_POLICY: 59, /** SABR_CONTEXT_SENDING_POLICY - Policy indicating which SABR contexts to send or discard in future requests. */ 59: "SABR_CONTEXT_SENDING_POLICY", LAWNMOWER_POLICY: 60, 60: "LAWNMOWER_POLICY", SABR_ACK: 61, 61: "SABR_ACK", END_OF_TRACK: 62, 62: "END_OF_TRACK", CACHE_LOAD_POLICY: 63, 63: "CACHE_LOAD_POLICY", LAWNMOWER_MESSAGING_POLICY: 64, 64: "LAWNMOWER_MESSAGING_POLICY", PREWARM_CONNECTION: 65, 65: "PREWARM_CONNECTION", PLAYBACK_DEBUG_INFO: 66, 66: "PLAYBACK_DEBUG_INFO", /** SNACKBAR_MESSAGE - Directive to show the user a notification message. */ SNACKBAR_MESSAGE: 67, /** SNACKBAR_MESSAGE - Directive to show the user a notification message. */ 67: "SNACKBAR_MESSAGE", UNRECOGNIZED: -1, "-1": "UNRECOGNIZED" }; //#endregion //#region node_modules/googlevideo/dist/src/utils/Logger.js var LogLevel = { NONE: 0, 0: "NONE", ERROR: 1, 1: "ERROR", WARN: 2, 2: "WARN", INFO: 3, 3: "INFO", DEBUG: 4, 4: "DEBUG", ALL: 99, 99: "ALL" }; /** * Singleton logger utility. * * Allows enabling or disabling specific log levels (`ERROR`, `WARN`, `INFO`, `DEBUG`) * at runtime. Supports logging with tags and message arguments. * * Usage: * ```ts * const logger = Logger.getInstance(); * logger.setLogLevels(LogLevel.ERROR, LogLevel.INFO); * logger.error('MyTag', 'An error occurred'); * ``` */ var Logger = class Logger { constructor() { this.currentLogLevels = /* @__PURE__ */ new Set([LogLevel.INFO, LogLevel.ERROR]); } static getInstance() { if (!Logger.instance) Logger.instance = new Logger(); return Logger.instance; } /** * Sets the active log levels. * Call with LogLevel.NONE or no arguments to turn off all logging. * Otherwise, specify one or more log levels to be active. * Use LogLevel.ALL to enable all log levels. */ setLogLevels(...levels) { if (levels.length === 0 || levels.includes(LogLevel.NONE)) this.currentLogLevels = /* @__PURE__ */ new Set(); else if (levels.includes(LogLevel.ALL)) this.currentLogLevels = /* @__PURE__ */ new Set([ LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG ]); else this.currentLogLevels = new Set(levels.filter((level) => level !== LogLevel.NONE && level !== LogLevel.ALL)); } /** * Gets the current set of active log levels. * @returns A new Set containing the active LogLevel enums. */ getLogLevels() { return new Set(this.currentLogLevels); } log(level, tag, ...messages) { if (level !== LogLevel.NONE && this.currentLogLevels.has(level)) { const prefix = `[${LogLevel[level]}] [${tag}]`; switch (level) { case LogLevel.ERROR: console.error(prefix, ...messages); break; case LogLevel.WARN: console.warn(prefix, ...messages); break; case LogLevel.INFO: console.info(prefix, ...messages); break; case LogLevel.DEBUG: console.debug(prefix, ...messages); } } } error(tag, ...messages) { this.log(LogLevel.ERROR, tag, ...messages); } warn(tag, ...messages) { this.log(LogLevel.WARN, tag, ...messages); } info(tag, ...messages) { this.log(LogLevel.INFO, tag, ...messages); } debug(tag, ...messages) { this.log(LogLevel.DEBUG, tag, ...messages); } }; //#endregion //#region node_modules/googlevideo/dist/src/utils/shared.js var MAX_INT32_VALUE = "2147483647"; var EnabledTrackTypes = { VIDEO_AND_AUDIO: 0, 0: "VIDEO_AND_AUDIO", AUDIO_ONLY: 1, 1: "AUDIO_ONLY", VIDEO_ONLY: 2, 2: "VIDEO_ONLY" }; /** * Converts a Base64 string to a Uint8Array. * @param base64 */ function base64ToU8(base64) { const standard_base64 = base64.replace(/-/g, "+").replace(/_/g, "/"); const padded_base64 = standard_base64.padEnd(standard_base64.length + (4 - standard_base64.length % 4) % 4, "="); return new Uint8Array(atob(padded_base64).split("").map((char) => char.charCodeAt(0))); } /** * Concatenates multiple Uint8Array chunks into a single Uint8Array. * @param chunks */ function concatenateChunks(chunks) { const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } /** * Returns a promise that resolves after a specified number of milliseconds. * @param ms - The number of milliseconds to wait. */ function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } //#endregion //#region node_modules/googlevideo/dist/src/utils/EventEmitterLike.js var __classPrivateFieldSet = function(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value; }; var __classPrivateFieldGet = function(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _CustomEvent_detail; var _EventEmitterLike_legacyListeners; var CustomEvent$1 = class extends Event { constructor(type, options) { super(type, options); _CustomEvent_detail.set(this, void 0); __classPrivateFieldSet(this, _CustomEvent_detail, options?.detail ?? null, "f"); } get detail() { return __classPrivateFieldGet(this, _CustomEvent_detail, "f"); } }; _CustomEvent_detail = /* @__PURE__ */ new WeakMap(); var EventEmitterLike = class extends EventTarget { constructor() { super(); _EventEmitterLike_legacyListeners.set(this, /* @__PURE__ */ new Map()); } emit(type, ...args) { const event = new CustomEvent$1(type, { detail: args }); this.dispatchEvent(event); } on(type, listener) { const wrapper = (ev) => { if (ev instanceof CustomEvent$1) listener(...ev.detail); else listener(ev); }; __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").set(listener, { type, wrapper }); this.addEventListener(type, wrapper); } once(type, listener) { const wrapper = (ev) => { if (ev instanceof CustomEvent$1) listener(...ev.detail); else listener(ev); this.off(type, listener); }; __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").set(listener, { type, wrapper }); this.addEventListener(type, wrapper); } off(type, listener) { const listenerData = __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").get(listener); if (listenerData && listenerData.type === type) { this.removeEventListener(type, listenerData.wrapper); __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").delete(listener); } } removeAllListeners(type) { if (type) { for (const [listener, listenerData] of __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").entries()) if (listenerData.type === type) { this.removeEventListener(type, listenerData.wrapper); __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").delete(listener); } } else for (const [listener, listenerData] of __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").entries()) { this.removeEventListener(listenerData.type, listenerData.wrapper); __classPrivateFieldGet(this, _EventEmitterLike_legacyListeners, "f").delete(listener); } } }; _EventEmitterLike_legacyListeners = /* @__PURE__ */ new WeakMap(); //#endregion //#region node_modules/googlevideo/dist/src/utils/formatKeyUtils.js /** * Creates a format key based on itag and xtags. * @returns A string format key. */ function createKey(itag, xtags) { return `${itag || ""}:${xtags || ""}`; } /** * Creates a format key from a SabrFormat object. * @returns A string format key or undefined if format is undefined. */ function fromFormat(format) { if (!format) return void 0; return createKey(format.itag, format.xtags); } /** * Creates a format key from a MediaHeader object. * @returns A string format key. */ function fromMediaHeader(mediaHeader) { return createKey(mediaHeader.itag, mediaHeader.xtags); } /** * Creates a format key from FormatInitializationMetadata. * @returns A string format key or undefined if formatId is undefined. */ function fromFormatInitializationMetadata(formatInitMetadata) { if (!formatInitMetadata.formatId) return ""; return createKey(formatInitMetadata.formatId.itag, formatInitMetadata.formatId.xtags); } //#endregion //#region node_modules/googlevideo/dist/src/utils/sabrStreamUtils.js /** * Determines the media type (video or audio) based on the format initialization metadata. * @param initializedFormat */ function getMediaType(initializedFormat) { return initializedFormat.formatInitializationMetadata?.mimeType?.includes("video") ? "video" : "audio"; } /** * Calculates the total duration of downloaded segments in milliseconds. * @param initializedFormat */ function getTotalDownloadedDuration(initializedFormat) { return Array.from(initializedFormat.downloadedSegments.values()).reduce((sum, segment) => sum + parseInt(segment.durationMs || "0"), 0); } /** * Filters formats by media type (audio or video) */ function filterFormatsByType(formats, isAudio) { return formats.filter((format) => { if (!format.mimeType) return false; return isAudio ? format.mimeType.includes("audio") : format.mimeType.includes("video"); }); } /** * Choose the best format based on options */ function chooseFormat(formats, formatOption, preferences) { if (!formats.length) return void 0; const typeFormats = filterFormatsByType(formats, preferences.isAudio); if (!typeFormats.length) return void 0; if (typeof formatOption === "number") return typeFormats.find((format) => format.itag === formatOption); if (formatOption && typeof formatOption !== "function") return formatOption; if (typeof formatOption === "function") return formatOption(typeFormats); let filteredFormats = typeFormats; if (preferences.language) filteredFormats = filteredFormats.filter((format) => format.language === preferences.language); if (preferences.quality) filteredFormats = filteredFormats.filter((format) => preferences.isAudio ? !!format.audioQuality?.toLowerCase().includes(preferences.quality?.toLowerCase() || "") : !!format.qualityLabel?.toLowerCase().includes(preferences.quality?.toLowerCase() || "")); if (preferences.isAudio) { if (preferences.preferOpus) filteredFormats = applyMimeTypeFilter(filteredFormats, "opus"); } else if (preferences.preferH264) filteredFormats = filteredFormats.filter((format) => !!format.mimeType && format.mimeType.includes("mp4") && format.mimeType.includes("avc")); if (preferences.preferWebM) filteredFormats = applyMimeTypeFilter(filteredFormats, "webm"); else if (preferences.preferMP4) filteredFormats = applyMimeTypeFilter(filteredFormats, "mp4"); return preferences.isAudio ? filteredFormats.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0] : filteredFormats.sort((a, b) => (b.height || 0) - (a.height || 0))[0]; } function applyMimeTypeFilter(formats, mimeTypePart) { return formats.filter((format) => format.mimeType?.includes(mimeTypePart)); } //#endregion //#region node_modules/googlevideo/dist/src/core/CompositeBuffer.js /** * A memory-efficient buffer that manages discontinuous chunks as a single logical stream. */ var CompositeBuffer = class CompositeBuffer { constructor(chunks = []) { this.chunks = []; this.currentChunkOffset = this.currentChunkIndex = 0; this.currentDataView = void 0; this.totalLength = 0; chunks.forEach((chunk) => this.append(chunk)); } append(chunk) { if (chunk instanceof Uint8Array) { if (this.canMergeWithLastChunk(chunk)) { const lastChunk = this.chunks[this.chunks.length - 1]; this.chunks[this.chunks.length - 1] = new Uint8Array(lastChunk.buffer, lastChunk.byteOffset, lastChunk.length + chunk.length); this.resetFocus(); } else this.chunks.push(chunk); this.totalLength += chunk.length; } else chunk.chunks.forEach((c) => this.append(c)); } split(position) { const extractedBuffer = new CompositeBuffer(); const remainingBuffer = new CompositeBuffer(); const iterator = this.chunks[Symbol.iterator](); let item = iterator.next(); while (!item.done) { const chunk = item.value; if (position >= chunk.length) { extractedBuffer.append(chunk); position -= chunk.length; } else if (position > 0) { extractedBuffer.append(new Uint8Array(chunk.buffer, chunk.byteOffset, position)); remainingBuffer.append(new Uint8Array(chunk.buffer, chunk.byteOffset + position, chunk.length - position)); position = 0; } else remainingBuffer.append(chunk); item = iterator.next(); } return { extractedBuffer, remainingBuffer }; } getLength() { return this.totalLength; } canReadBytes(position, length) { return position + length <= this.totalLength; } getUint8(position) { this.focus(position); return this.chunks[this.currentChunkIndex][position - this.currentChunkOffset]; } focus(position) { if (!this.isFocused(position)) { if (position < this.currentChunkOffset) this.resetFocus(); while (this.currentChunkOffset + this.chunks[this.currentChunkIndex].length <= position && this.currentChunkIndex < this.chunks.length - 1) { this.currentChunkOffset += this.chunks[this.currentChunkIndex].length; this.currentChunkIndex += 1; } this.currentDataView = void 0; } } isFocused(position) { return position >= this.currentChunkOffset && position < this.currentChunkOffset + this.chunks[this.currentChunkIndex].length; } resetFocus() { this.currentDataView = void 0; this.currentChunkIndex = 0; this.currentChunkOffset = 0; } canMergeWithLastChunk(chunk) { if (this.chunks.length === 0) return false; const lastChunk = this.chunks[this.chunks.length - 1]; return lastChunk.buffer === chunk.buffer && lastChunk.byteOffset + lastChunk.length === chunk.byteOffset; } }; //#endregion //#region node_modules/googlevideo/dist/src/core/UmpReader.js /** * A parser that efficiently processes chunked UMP binary data. */ var UmpReader = class { constructor(compositeBuffer) { this.compositeBuffer = compositeBuffer; } /** * Parses parts from the buffer and calls the handler for each complete part. * @param handlePart - Function called with each complete part. * @returns Partial part if parsing is incomplete, undefined otherwise. */ read(handlePart) { while (true) { let offset = 0; const [partType, newOffset] = this.readVarInt(offset); offset = newOffset; const [partSize, finalOffset] = this.readVarInt(offset); offset = finalOffset; if (partType < 0 || partSize < 0) break; if (!this.compositeBuffer.canReadBytes(offset, partSize)) { if (!this.compositeBuffer.canReadBytes(offset, 1)) break; return { type: partType, size: partSize, data: this.compositeBuffer }; } const splitResult = this.compositeBuffer.split(offset).remainingBuffer.split(partSize); offset = 0; handlePart({ type: partType, size: partSize, data: splitResult.extractedBuffer }); this.compositeBuffer = splitResult.remainingBuffer; } } /** * Reads a variable-length integer from the buffer. * @param offset - Position to start reading from. * @returns Tuple of [value, new offset] or [-1, offset] if incomplete. */ readVarInt(offset) { let byteLength; if (this.compositeBuffer.canReadBytes(offset, 1)) { const firstByte = this.compositeBuffer.getUint8(offset); byteLength = firstByte < 128 ? 1 : firstByte < 192 ? 2 : firstByte < 224 ? 3 : firstByte < 240 ? 4 : 5; } else byteLength = 0; if (byteLength < 1 || !this.compositeBuffer.canReadBytes(offset, byteLength)) return [-1, offset]; let value; switch (byteLength) { case 1: value = this.compositeBuffer.getUint8(offset++); break; case 2: { const byte1 = this.compositeBuffer.getUint8(offset++); const byte2 = this.compositeBuffer.getUint8(offset++); value = (byte1 & 63) + 64 * byte2; break; } case 3: { const byte1 = this.compositeBuffer.getUint8(offset++); const byte2 = this.compositeBuffer.getUint8(offset++); const byte3 = this.compositeBuffer.getUint8(offset++); value = (byte1 & 31) + 32 * (byte2 + 256 * byte3); break; } case 4: { const byte1 = this.compositeBuffer.getUint8(offset++); const byte2 = this.compositeBuffer.getUint8(offset++); const byte3 = this.compositeBuffer.getUint8(offset++); const byte4 = this.compositeBuffer.getUint8(offset++); value = (byte1 & 15) + 16 * (byte2 + 256 * (byte3 + 256 * byte4)); break; } default: { const tempOffset = offset + 1; this.compositeBuffer.focus(tempOffset); if (this.canReadFromCurrentChunk(tempOffset, 4)) value = this.getCurrentDataView().getUint32(tempOffset - this.compositeBuffer.currentChunkOffset, true); else { const byte3 = this.compositeBuffer.getUint8(tempOffset + 2) + 256 * this.compositeBuffer.getUint8(tempOffset + 3); value = this.compositeBuffer.getUint8(tempOffset) + 256 * (this.compositeBuffer.getUint8(tempOffset + 1) + 256 * byte3); } offset += 5; break; } } return [value, offset]; } /** * Checks if the specified bytes can be read from the current chunk. * @param offset - Position to start reading from. * @param length - Number of bytes to read. * @returns True if bytes can be read from current chunk, false otherwise. */ canReadFromCurrentChunk(offset, length) { return offset - this.compositeBuffer.currentChunkOffset + length <= this.compositeBuffer.chunks[this.compositeBuffer.currentChunkIndex].length; } /** * Gets a DataView of the current chunk, creating it if necessary. * @returns DataView for the current chunk. */ getCurrentDataView() { if (!this.compositeBuffer.currentDataView) { const currentChunk = this.compositeBuffer.chunks[this.compositeBuffer.currentChunkIndex]; this.compositeBuffer.currentDataView = new DataView(currentChunk.buffer, currentChunk.byteOffset, currentChunk.length); } return this.compositeBuffer.currentDataView; } }; //#endregion //#region node_modules/googlevideo/dist/src/core/SabrStream.js var TAG = "SabrStream"; var DEFAULT_MAX_RETRIES = 10; var MAX_BACKOFF_MS = 8e3; var BACKOFF_MULTIPLIER = 500; var DEFAULT_STALL_DETECTION_MS = 3e4; var MAX_STALLS = 5; /** * Manages the download and processing of YouTube's Server-Adaptive Bitrate (SABR) streams. * * This class handles the entire lifecycle of a SABR stream: * - Selecting appropriate video and audio formats. * - Making network requests to fetch media segments. * - Processing UMP parts in real-time. * - Handling server-side directives like redirects, context updates, and backoff policies. * - Emitting events for key stream updates, such as format initialization and errors. * - Providing separate `ReadableStream` instances for video and audio data. */ var SabrStream = class extends EventEmitterLike { on(event, listener) { super.on(event, listener); } once(event, listener) { super.once(event, listener); } constructor(config = {}) { super(); this.logger = Logger.getInstance(); this.formatIds = []; this.umpPartHandlers = /* @__PURE__ */ new Map([ [UMPPartId.FORMAT_INITIALIZATION_METADATA, this.handleFormatInitializationMetadata.bind(this)], [UMPPartId.NEXT_REQUEST_POLICY, this.handleNextRequestPolicy.bind(this)], [UMPPartId.SABR_ERROR, this.handleSabrError.bind(this)], [UMPPartId.SABR_REDIRECT, this.handleSabrRedirect.bind(this)], [UMPPartId.SABR_CONTEXT_UPDATE, this.handleSabrContextUpdate.bind(this)], [UMPPartId.SABR_CONTEXT_SENDING_POLICY, this.handleSabrContextSendingPolicy.bind(this)], [UMPPartId.STREAM_PROTECTION_STATUS, this.handleStreamProtectionStatus.bind(this)], [UMPPartId.RELOAD_PLAYER_RESPONSE, this.handleReloadPlayerResponse.bind(this)], [UMPPartId.MEDIA_HEADER, this.handleMediaHeader.bind(this)], [UMPPartId.MEDIA, this.handleMedia.bind(this)], [UMPPartId.MEDIA_END, this.handleMediaEnd.bind(this)] ]); this.sabrContexts = /* @__PURE__ */ new Map(); this.activeSabrContextTypes = /* @__PURE__ */ new Set(); this.initializedFormatsMap = /* @__PURE__ */ new Map(); this.partialSegmentQueue = /* @__PURE__ */ new Map(); this.requestNumber = 0; this.durationMs = Infinity; this.mediaHeadersProcessed = false; this._errored = false; this._aborted = false; this.progressTracker = { lastProgressTime: Date.now(), lastDownloadedDuration: 0, stallCount: 0 }; this.fetchFunction = config?.fetch || fetch; this.serverAbrStreamingUrl = config.serverAbrStreamingUrl; this.videoPlaybackUstreamerConfig = config.videoPlaybackUstreamerConfig; this.clientInfo = config.clientInfo; this.poToken = config.poToken; this.durationMs = config.durationMs || Infinity; this.formatIds = config.formats || []; this.videoStream = new ReadableStream({ start: (controller) => { this.videoController = controller; } }); this.audioStream = new ReadableStream({ start: (controller) => { this.audioController = controller; } }); } /** * Sets Proof of Origin (PO) token. * @param poToken - The base64-encoded token string. */ setPoToken(poToken) { this.poToken = poToken; } /** * Sets the available server ABR formats. * @param formats - An array of available SabrFormat objects. */ setServerAbrFormats(formats) { this.formatIds.push(...formats); } /** * Sets the total duration of the stream in milliseconds. * This is optional as duration is often determined automatically from format metadata. * @param durationMs - The duration in milliseconds. */ setDurationMs(durationMs) { this.durationMs = durationMs; } /** * Sets the server ABR streaming URL for media requests. * @param url - The streaming URL. */ setStreamingURL(url) { this.serverAbrStreamingUrl = url; } /** * Sets the Ustreamer configuration string. * @param config - The Ustreamer configuration. */ setUstreamerConfig(config) { this.videoPlaybackUstreamerConfig = config; } /** * Sets the client information used in SABR requests. * @param clientInfo - The client information object. */ setClientInfo(clientInfo) { this.clientInfo = clientInfo; } /** * Aborts the download process, closing all streams and cleaning up resources. * Emits an 'abort' event. */ abort() { this.logger.debug(TAG, "Aborting download process"); this._aborted = true; this.abortController?.abort(); this.videoController?.error(/* @__PURE__ */ new Error("Download aborted.")); this.audioController?.error(/* @__PURE__ */ new Error("Download aborted.")); this.resetState(); this.emit("abort"); } /** * Returns a serializable state object that can be used to restore the stream later. * @throws {Error} If the main format is not initialized. * @returns The current state of the stream. */ getState() { if (!this.mainFormat) throw new Error("Main format is not initialized, cannot get state."); const playerTimeMs = getTotalDownloadedDuration(this.mainFormat); const initializedFormats = []; for (const [formatKey, format] of this.initializedFormatsMap.entries()) initializedFormats.push({ formatKey, formatInitializationMetadata: format.formatInitializationMetadata, downloadedSegments: Array.from(format.downloadedSegments.entries()), lastMediaHeaders: format.lastMediaHeaders }); return { durationMs: this.durationMs, requestNumber: this.requestNumber, activeSabrContexts: Array.from(this.activeSabrContextTypes), sabrContextUpdates: Array.from(this.sabrContexts.entries()), formatToDiscard: this.formatToDiscard, cachedBufferedRanges: this.cachedBufferedRanges || [], nextRequestPolicy: this.nextRequestPolicy, initializedFormats, playerTimeMs }; } /** * Initiates the streaming process for the selected formats. * @param options - Playback options, including format preferences and initial state. * @throws {Error} If no suitable formats are found or streaming fails. * @returns A promise that resolves with the video/audio streams and selected formats. */ async start(options) { const { videoFormat, audioFormat } = this.selectFormats(options); this.setupStreamingProcess(videoFormat, audioFormat, options).then(); return { videoStream: this.videoStream, audioStream: this.audioStream, selectedFormats: { videoFormat, audioFormat } }; } /** * Sets up and manages the main streaming loop. * @param videoFormat - The selected video format. * @param audioFormat - The selected audio format. * @param options - Playback options. * @private */ async setupStreamingProcess(videoFormat, audioFormat, options) { try { this._errored = false; this._aborted = false; let playerTimeMs = 0; if (options.state && this.restoreState(videoFormat, audioFormat, options.state)) playerTimeMs = options.state.playerTimeMs || 0; const maxRetries = options.maxRetries !== void 0 ? options.maxRetries : DEFAULT_MAX_RETRIES; const enabledTrackTypesBitfield = options.enabledTrackTypes ?? EnabledTrackTypes.VIDEO_AND_AUDIO; const abrState = { playerTimeMs, audioTrackId: audioFormat.audioTrackId, playbackRate: 1, stickyResolution: videoFormat.height || 360, drcEnabled: audioFormat.isDrc, clientViewportIsFlexible: false, visibility: 1, enabledTrackTypesBitfield }; if (abrState.enabledTrackTypesBitfield === 1 || abrState.enabledTrackTypesBitfield === 2) this.formatToDiscard = abrState.enabledTrackTypesBitfield === 1 ? fromFormat(videoFormat) : fromFormat(audioFormat); while (parseInt(abrState.playerTimeMs) < this.durationMs) { if (this._aborted) { this.logger.debug(TAG, "Download process aborted, exiting streaming loop."); break; } this.logger.debug(TAG, `Starting new segment fetch at playback position: ${abrState.playerTimeMs}ms`); this.mainFormat = abrState.enabledTrackTypesBitfield === 1 ? this.initializedFormatsMap.get(fromFormat(audioFormat) || "") : this.initializedFormatsMap.get(fromFormat(videoFormat) || ""); if (this.mainFormat) this.validateAndCorrectDuration(this.mainFormat.formatInitializationMetadata); abrState.playerTimeMs = this.mainFormat ? getTotalDownloadedDuration(this.mainFormat) : 0; const { shouldStop } = this.checkForStall({ playerTimeMs: abrState.playerTimeMs, stallDetectionMs: options.stallDetectionMs }); if (shouldStop) break; abrState.playerTimeMs = abrState.playerTimeMs.toString(); if (!await this.executeWithRetry(() => this.fetchAndProcessSegments(abrState, audioFormat, videoFormat), maxRetries)) break; } } catch (error) { if (!this._aborted) this.errorHandler(error, true); } finally { if (!this._aborted) { this.validateDownloadedSegments(); if (!this._errored) { this.videoController?.close(); this.audioController?.close(); } this.resetState(); this.emit("finish"); } } } /** * Restores the stream state from a previously saved state object. * @param videoFormat - The selected video format. * @param audioFormat - The selected audio format. * @param state - The saved state object. * @returns `true` if the state was restored successfully, `false` otherwise. * @private */ restoreState(videoFormat, audioFormat, state) { this.resetState(); if (!state || typeof state !== "object" || !state.initializedFormats || !Array.isArray(state.initializedFormats) || !state.durationMs || !state.playerTimeMs) { this.logger.warn(TAG, "Invalid or corrupt state object provided. Starting fresh."); return false; } const expectedVideoFormatKey = fromFormat(videoFormat) || ""; const expectedAudioFormatKey = fromFormat(audioFormat) || ""; for (const format of state.initializedFormats) { const { formatKey, formatInitializationMetadata, downloadedSegments, lastMediaHeaders } = format; if (formatKey !== expectedVideoFormatKey && formatKey !== expectedAudioFormatKey) { this.logger.warn(TAG, `State contains an unexpected format key "${formatKey}". It will be ignored.`); continue; } this.initializedFormatsMap.set(formatKey, { formatInitializationMetadata, downloadedSegments: new Map(downloadedSegments), lastMediaHeaders: lastMediaHeaders || [] }); } if (!this.initializedFormatsMap.has(expectedVideoFormatKey) || !this.initializedFormatsMap.has(expectedAudioFormatKey)) { this.logger.warn(TAG, "State is missing required format data for the selected video/audio formats. Starting fresh."); this.resetState(); return false; } this.durationMs = state.durationMs; this.requestNumber = state.requestNumber || 0; this.activeSabrContextTypes = new Set(state.activeSabrContexts || []); this.sabrContexts = new Map(state.sabrContextUpdates || []); this.formatToDiscard = state.formatToDiscard; this.cachedBufferedRanges = state.cachedBufferedRanges || []; this.nextRequestPolicy = state.nextRequestPolicy; return true; } /** * Checks if the download has stalled by tracking progress over time. * @param options - Configuration for stall detection. * @returns An object indicating whether the stream should stop and if it is stalled. * @throws {Error} If the maximum number of consecutive stalls is reached. * @private */ checkForStall(options) { const currentTime = Date.now(); const currentProgress = options.playerTimeMs; const stallThreshold = options.stallDetectionMs || DEFAULT_STALL_DETECTION_MS; if (currentProgress > this.progressTracker.lastDownloadedDuration) { this.progressTracker.lastProgressTime = currentTime; this.progressTracker.lastDownloadedDuration = currentProgress; this.progressTracker.stallCount = 0; return { shouldStop: false, stalled: false }; } else if (currentTime - this.progressTracker.lastProgressTime > stallThreshold) { this.progressTracker.stallCount++; this.logger.warn(TAG, `Stream stalled for ${stallThreshold}ms (stall #${this.progressTracker.stallCount})`); if (this.progressTracker.stallCount >= MAX_STALLS) throw new Error(`Stream stalled ${MAX_STALLS} times, aborting`); this.progressTracker.lastProgressTime = currentTime; if (Math.abs(this.durationMs - currentProgress) < 5e3) { this.logger.warn(TAG, "Stream is close to completion, but stalled. Checking if we have the last segment."); const endSegmentNumber = parseInt(this.mainFormat?.formatInitializationMetadata.endSegmentNumber || "0") || -1; const lastSegment = this.mainFormat?.downloadedSegments.get(endSegmentNumber); if (lastSegment && lastSegment.segmentNumber === endSegmentNumber) { this.logger.warn(TAG, "Last segment is already downloaded. Stopping further processing."); return { shouldStop: true, stalled: true }; } } return { shouldStop: false, stalled: true }; } return { shouldStop: false, stalled: false }; } /** * Selects the best video and audio formats based on provided options. * @param options - Format selection options and quality preferences. * @throws {Error} If no suitable formats are found or the duration is invalid. * @returns The selected video and audio formats. * @private */ selectFormats(options) { const videoFormat = chooseFormat(this.formatIds, options.videoFormat, { quality: options.videoQuality, preferWebM: options.preferWebM, preferH264: options.preferH264, preferMP4: options.preferMP4, isAudio: false }); const audioFormat = chooseFormat(this.formatIds, options.audioFormat, { quality: options.audioQuality, language: options.audioLanguage, preferOpus: options.preferOpus, preferMP4: options.preferMP4, preferWebM: options.preferWebM, isAudio: true }); if (this.durationMs < 0) throw new Error("Invalid duration"); if (!videoFormat || !audioFormat) throw new Error("No suitable formats found for download"); return { videoFormat, audioFormat }; } /** * Fetches and processes media segments from the server for the current ABR state. * @param abrState - The current client adaptive bitrate state. * @param selectedAudioFormat - The selected audio format. * @param selectedVideoFormat - The selected video format. * @throws {Error} If the server returns an error or no valid data. * @private */ async fetchAndProcessSegments(abrState, selectedAudioFormat, selectedVideoFormat) { const initializedVideoFormat = this.initializedFormatsMap.get(fromFormat(selectedVideoFormat) || ""); const initializedAudioFormat = this.initializedFormatsMap.get(fromFormat(selectedAudioFormat) || ""); if (!this.cachedBufferedRanges?.length) this.cachedBufferedRanges = this.buildBufferedRanges(initializedVideoFormat, initializedAudioFormat); const requestBody = this.buildRequestBody(abrState, selectedAudioFormat, selectedVideoFormat); this.mediaHeadersProcessed = false; const response = await this.makeStreamingRequest(requestBody); const processedParts = await this.processStreamingResponse(response); if (!processedParts.length) throw new Error("No valid parts received from server."); else if ((this.streamProtectionStatus?.status || 0) >= 2 && !processedParts.includes(UMPPartId.MEDIA)) throw new Error("No media parts or protocol updates received from server."); if (processedParts.includes(UMPPartId.MEDIA_HEADER) && initializedVideoFormat?.lastMediaHeaders?.length && initializedAudioFormat?.lastMediaHeaders?.length || abrState.enabledTrackTypesBitfield !== 0 && this.mainFormat?.lastMediaHeaders?.length) this.mediaHeadersProcessed = true; } /** * Constructs an array of `BufferedRange` objects from initialized formats. * @param initializedVideoFormat - The initialized video format, if available. * @param initializedAudioFormat - The initialized audio format, if available. * @returns An array of `BufferedRange` objects. * @private */ buildBufferedRanges(initializedVideoFormat, initializedAudioFormat) { const bufferedRanges = []; const formats = [initializedVideoFormat, initializedAudioFormat]; for (const initializedFormat of formats) { if (!initializedFormat?.lastMediaHeaders.length) continue; if (fromFormatInitializationMetadata(initializedFormat.formatInitializationMetadata) === this.formatToDiscard) continue; const mediaHeaders = initializedFormat.lastMediaHeaders; const durationMs = mediaHeaders.reduce((sum, header) => sum + parseInt(header.durationMs || "0"), 0); bufferedRanges.push({ durationMs: durationMs.toString(), formatId: initializedFormat.formatInitializationMetadata.formatId, startTimeMs: String(mediaHeaders[0].startMs || "0"), startSegmentIndex: mediaHeaders[0].sequenceNumber || 1, endSegmentIndex: mediaHeaders[mediaHeaders.length - 1].sequenceNumber || 1, timeRange: { durationTicks: durationMs.toString(), startTicks: mediaHeaders[0].startMs, timescale: mediaHeaders[0].timeRange?.timescale } }); initializedFormat.lastMediaHeaders = []; } return bufferedRanges; } /** * Builds the protobuf request body for a `VideoPlaybackAbrRequest`. * @param abrState - The current client adaptive bitrate state. * @param selectedAudioFormat - The selected audio format. * @param selectedVideoFormat - The selected video format. * @returns The encoded request body as a `Uint8Array`. * @throws {Error} If required configuration (ustreamer config, client info) is missing. * @private */ buildRequestBody(abrState, selectedAudioFormat, selectedVideoFormat) { if (!this.videoPlaybackUstreamerConfig) throw new Error("Video playback ustreamer config must be set before starting."); if (!this.clientInfo) throw new Error("Client info must be set before starting."); const bufferedRanges = this.cachedBufferedRanges || []; const { sabrContexts, unsentSabrContexts } = this.prepareSabrContexts(); const { selectedFormatIds, updatedBufferedRanges } = this.prepareFormatSelections([selectedVideoFormat, selectedAudioFormat], bufferedRanges); return VideoPlaybackAbrRequest.encode({ clientAbrState: abrState, preferredAudioFormatIds: [selectedAudioFormat], preferredVideoFormatIds: [selectedVideoFormat], preferredSubtitleFormatIds: [], selectedFormatIds, videoPlaybackUstreamerConfig: base64ToU8(this.videoPlaybackUstreamerConfig), streamerContext: { sabrContexts, unsentSabrContexts, poToken: this.poToken ? base64ToU8(this.poToken) : void 0, playbackCookie: this.nextRequestPolicy?.playbackCookie ? PlaybackCookie.encode(this.nextRequestPolicy.playbackCookie).finish() : void 0, clientInfo: this.clientInfo }, bufferedRanges: updatedBufferedRanges, field1000: [] }).finish(); } /** * Prepares SABR context data for the request body. * @returns An object containing active and unsent SABR contexts. * @private */ prepareSabrContexts() { const sabrContexts = []; const unsentSabrContexts = []; for (const ctxUpdate of this.sabrContexts.values()) if (this.activeSabrContextTypes.has(ctxUpdate.type)) sabrContexts.push(ctxUpdate); else unsentSabrContexts.push(ctxUpdate.type); return { sabrContexts, unsentSabrContexts }; } /** * Prepares format selections and buffered ranges for the request body. * @param formats - An array of formats to process. * @param currentBufferedRanges - The current buffered ranges to update. * @returns An object with selected format IDs and updated buffered ranges. * @private */ prepareFormatSelections(formats, currentBufferedRanges) { const selectedFormatIds = []; const updatedBufferedRanges = [...currentBufferedRanges]; const formatsInitialized = this.initializedFormatsMap.size > 0; for (const format of formats) { const formatKey = fromFormat(format); const shouldDiscard = this.formatToDiscard && formatKey === this.formatToDiscard; if (shouldDiscard) updatedBufferedRanges.push({ formatId: format, durationMs: MAX_INT32_VALUE, startTimeMs: String(0), startSegmentIndex: parseInt(MAX_INT32_VALUE), endSegmentIndex: parseInt(MAX_INT32_VALUE), timeRange: { durationTicks: MAX_INT32_VALUE, startTicks: "0", timescale: 1e3 } }); if (formatsInitialized || shouldDiscard) selectedFormatIds.push(format); } return { selectedFormatIds, updatedBufferedRanges }; } /** * Executes a streaming POST request to the server. * @param body - The request body payload. * @returns A `Promise` that resolves with the server `Response`. * @throws {Error} If the server ABR streaming URL is not configured or the request fails. * @private */ async makeStreamingRequest(body) { if (!this.serverAbrStreamingUrl) throw new Error("Server ABR streaming URL not configured."); const url = new URL(this.serverAbrStreamingUrl); url.searchParams.set("rn", this.requestNumber.toString()); this.abortController = new AbortController(); const timeoutId = setTimeout(() => this.abortController?.abort(), 6e4); try { return await this.fetchFunction(url, { method: "POST", headers: { "content-type": "application/x-protobuf", "accept-encoding": "identity", "accept": "application/vnd.yt-ump" }, body, signal: this.abortController.signal }); } finally { clearTimeout(timeoutId); this.requestNumber += 1; } } /** * Reads the response body as a stream and processes each UMP part. * @param response - The server response to process. * @returns A promise that resolves to an array of processed UMP part types. * @throws {Error} If the response is invalid, empty, or aborted. * @private */ async processStreamingResponse(response) { if (!response.ok) throw new Error(`Server returned ${response.status} ${response.statusText}`); if (response.headers.get("content-type") !== "application/vnd.yt-ump") throw new Error(`Unexpected content type from server: ${response.headers.get("content-type")}`); const reader = response.body.getReader(); let dataReceived = false; let partialPart; const processedParts = []; while (true) { if (this.abortController?.signal?.aborted && !this._aborted) throw new Error("Stream was aborted."); const { done, value } = await reader.read(); if (done) { if (!dataReceived) throw new Error("Received empty response from server."); break; } dataReceived = true; let chunk; if (partialPart) { chunk = partialPart.data; chunk.append(value); } else chunk = new CompositeBuffer([value]); partialPart = new UmpReader(chunk).read((part) => { processedParts.push(part.type); const handler = this.umpPartHandlers.get(part.type); if (handler) handler(part); }); } return processedParts; } /** * Executes a function with automatic retries and exponential backoff. * Respects server-specified backoff times from `nextRequestPolicy`. * @param fetchFn - The function to execute. * @param maxRetries - The maximum number of retry attempts. * @returns A promise that resolves to `true` on success, or `false` if all retries fail. * @private */ async executeWithRetry(fetchFn, maxRetries) { const backoffTimeMs = this.nextRequestPolicy?.backoffTimeMs || 0; if (backoffTimeMs > 0) { this.logger.debug(TAG, `Respecting server backoff policy: waiting ${backoffTimeMs}ms before request`); await wait(backoffTimeMs); } for (let attempt = 1; attempt <= maxRetries + 1; attempt++) try { await fetchFn(); if (this.mediaHeadersProcessed) this.cachedBufferedRanges = void 0; return true; } catch (e) { const error = e; if (this._aborted) { this.logger.debug(TAG, "Download process aborted, skipping retry."); return false; } if (attempt > maxRetries) { this.logger.error(TAG, `Maximum retries (${maxRetries}) exceeded while fetching segment: ${error.message}`); this.errorHandler(error, true); break; } const retryBackoffMs = Math.min(BACKOFF_MULTIPLIER * Math.pow(2, attempt - 1), MAX_BACKOFF_MS); this.logger.warn(TAG, `Segment fetch attempt ${attempt}/${maxRetries + 1} failed - retrying in ${retryBackoffMs}ms`, error); await wait(retryBackoffMs); } finally { this.partialSegmentQueue.clear(); } return false; } /** * Decodes a UMP part using the provided decoder. * @param part * @param decoder * @private */ decodePart(part, decoder) { if (!part.data.chunks.length) return void 0; try { return decoder.decode(concatenateChunks(part.data.chunks)); } catch { return; } } /** * Handles `FORMAT_INITIALIZATION_METADATA` parts. * Creates and stores a new `InitializedFormat` entry. * @private */ handleFormatInitializationMetadata(part) { const formatInitMetadata = this.decodePart(part, FormatInitializationMetadata); if (!formatInitMetadata) return; const formatIdKey = fromFormatInitializationMetadata(formatInitMetadata); const initializedFormat = { formatInitializationMetadata: formatInitMetadata, downloadedSegments: /* @__PURE__ */ new Map(), lastMediaHeaders: [] }; this.initializedFormatsMap.set(formatIdKey, initializedFormat); this.logger.debug(TAG, `Initialized format: ${formatIdKey}`); this.emit("formatInitialization", initializedFormat); } /** * Handles `NEXT_REQUEST_POLICY` parts. * Stores the server's policy for backoff time and playback cookies. * @private */ handleNextRequestPolicy(part) { this.nextRequestPolicy = this.decodePart(part, NextRequestPolicy); } /** * Handles `SABR_ERROR` parts. * Throws an error to terminate the current request attempt. * @throws {Error} Always throws with the SABR error details. * @private */ handleSabrError(part) { const sabrError = this.decodePart(part, SabrError); if (!sabrError) return; throw new Error(`SABR Error: ${sabrError.type} - ${sabrError.code}`); } /** * Handles `SABR_REDIRECT` parts. * Updates the streaming URL to the new location provided by the server. * @private */ handleSabrRedirect(part) { const sabrRedirect = this.decodePart(part, SabrRedirect); if (!sabrRedirect) return; if (sabrRedirect.url) { this.serverAbrStreamingUrl = sabrRedirect.url; this.logger.debug(TAG, `Redirecting to ${this.serverAbrStreamingUrl}`); } } /** * Handles `SABR_CONTEXT_UPDATE` parts. * Updates the client's context state based on server instructions. * @private */ handleSabrContextUpdate(part) { const sabrContextUpdate = this.decodePart(part, SabrContextUpdate); if (!sabrContextUpdate) return; if (sabrContextUpdate.type !== void 0 && sabrContextUpdate.value?.length) { if (sabrContextUpdate.writePolicy === SabrContextUpdate_SabrContextWritePolicy.KEEP_EXISTING && this.sabrContexts.has(sabrContextUpdate.type)) { this.logger.debug(TAG, `Skipping SABR context update for type ${sabrContextUpdate.type}`); return; } this.sabrContexts.set(sabrContextUpdate.type, sabrContextUpdate); if (sabrContextUpdate.sendByDefault) this.activeSabrContextTypes.add(sabrContextUpdate.type); this.logger.debug(TAG, `Received SABR context update (type: ${sabrContextUpdate.type}, sendByDefault: ${sabrContextUpdate.sendByDefault})`); } } /** * Handles `SABR_CONTEXT_SENDING_POLICY` parts. * Updates which contexts should be sent in future requests. * @private */ handleSabrContextSendingPolicy(part) { const sabrContextSendingPolicy = this.decodePart(part, SabrContextSendingPolicy); if (!sabrContextSendingPolicy) return; for (const startPolicy of sabrContextSendingPolicy.startPolicy) if (!this.activeSabrContextTypes.has(startPolicy)) { this.activeSabrContextTypes.add(startPolicy); this.logger.debug(TAG, `Activated SABR context for type ${startPolicy}`); } for (const stopPolicy of sabrContextSendingPolicy.stopPolicy) if (this.activeSabrContextTypes.has(stopPolicy)) { this.activeSabrContextTypes.delete(stopPolicy); this.logger.debug(TAG, `Deactivated SABR context for type ${stopPolicy}`); } for (const discardPolicy of sabrContextSendingPolicy.discardPolicy) if (this.sabrContexts.has(discardPolicy)) { this.sabrContexts.delete(discardPolicy); this.logger.debug(TAG, `Discarded SABR context for type ${discardPolicy}`); } } /** * Handles `STREAM_PROTECTION_STATUS` parts. * Emits updates and handles critical statuses like required attestation. * @throws {Error} If attestation is required (status 3). * @private */ handleStreamProtectionStatus(part) { this.streamProtectionStatus = this.decodePart(part, StreamProtectionStatus); if (!this.streamProtectionStatus) return; this.emit("streamProtectionStatusUpdate", this.streamProtectionStatus); if (this.streamProtectionStatus.status === 3) throw new Error("Cannot proceed with stream: attestation required"); else if (this.streamProtectionStatus.status === 2) this.logger.warn(TAG, "Attestation pending."); } /** * Handles `RELOAD_PLAYER_RESPONSE` parts. * Emits an event with reload parameters and terminates the session. * @throws {Error} Always throws to terminate the current streaming session. * @private */ handleReloadPlayerResponse(part) { const reloadPlaybackContext = this.decodePart(part, ReloadPlaybackContext); if (!reloadPlaybackContext) return; const errorMessage = "Player response reload requested by server"; this.logger.debug(TAG, `${errorMessage} (token: ${reloadPlaybackContext.reloadPlaybackParams?.token}`); this.emit("reloadPlayerResponse", reloadPlaybackContext); throw new Error(errorMessage); } /** * Handles `MEDIA_HEADER` parts. * Creates an entry in the `partialSegmentQueue` for the upcoming media chunks. * @private */ handleMediaHeader(part) { const mediaHeader = this.decodePart(part, MediaHeader); if (!mediaHeader) return; const headerId = mediaHeader.headerId || 0; const formatIdKey = fromMediaHeader(mediaHeader); const segmentNumber = mediaHeader.isInitSeg ? 0 : mediaHeader.sequenceNumber || 0; const durationMs = mediaHeader.durationMs || Math.ceil(parseInt(mediaHeader.timeRange?.durationTicks || "0") / (mediaHeader.timeRange?.timescale || 0) * 1e3).toString(); const initializedFormat = this.initializedFormatsMap.get(formatIdKey); if (!initializedFormat) { this.logger.warn(TAG, `No initialized format found for key: ${formatIdKey} (segment ${segmentNumber})`); return; } const mediaType = getMediaType(initializedFormat); if (initializedFormat.downloadedSegments.has(segmentNumber)) { this.logger.debug(TAG, `Segment ${formatIdKey} (segment: ${segmentNumber}) already downloaded. Ignoring.`); return; } this.partialSegmentQueue.set(headerId, { formatIdKey, segmentNumber, durationMs, mediaHeader, bufferedChunks: [] }); this.logger.debug(TAG, `Enqueued ${mediaType} segment ${segmentNumber} (Header ID: ${headerId}, key: ${formatIdKey}, duration: ${durationMs}ms)`); } /** * Handles `MEDIA` parts. * Buffers media data chunks associated with a specific header ID. * @private */ handleMedia(part) { const headerId = part.data.getUint8(0); const segment = this.partialSegmentQueue.get(headerId); if (!segment) { this.logger.debug(TAG, `Received Media part for an unknown Header ID: ${headerId}`); return; } if (!this.initializedFormatsMap.get(segment.formatIdKey)) { this.logger.warn(TAG, `No initialized format found for key ${segment.formatIdKey} (segment ${segment.segmentNumber})`); return; } const dataBuffer = part.data.split(1).remainingBuffer; for (const chunk of dataBuffer.chunks) segment.bufferedChunks.push(chunk); } /** * Handles `MEDIA_END` parts. * Finalizes a segment, enqueues its data to the appropriate stream, and updates tracking. * @private */ handleMediaEnd(part) { const headerId = part.data.getUint8(0); const segment = this.partialSegmentQueue.get(headerId); if (!segment) { this.logger.debug(TAG, `Received MediaEnd for an unknown Header ID: ${headerId}`); return; } const loadedBytes = segment.bufferedChunks.reduce((sum, chunk) => sum + chunk.length, 0); if (loadedBytes !== parseInt(segment.mediaHeader.contentLength || "0")) { this.logger.warn(TAG, `Content length mismatch for segment ${segment.segmentNumber} (Header ID: ${headerId}, key: ${segment.formatIdKey}, expected: ${segment.mediaHeader.contentLength}, received: ${loadedBytes})`); this.partialSegmentQueue.delete(headerId); return; } const initializedFormat = this.initializedFormatsMap.get(segment.formatIdKey); if (initializedFormat) { const mediaType = getMediaType(initializedFormat); if (segment.bufferedChunks.length) for (const chunk of segment.bufferedChunks) if (mediaType === "audio") this.audioController?.enqueue(chunk); else this.videoController?.enqueue(chunk); this.logger.debug(TAG, `Received MediaEnd for ${mediaType} segment ${segment.segmentNumber} (Header ID: ${headerId}, key: ${segment.formatIdKey})`); segment.bufferedChunks.length = 0; segment.bufferedChunks = []; initializedFormat.lastMediaHeaders.push(segment.mediaHeader); initializedFormat.downloadedSegments.set(segment.segmentNumber, segment); this.partialSegmentQueue.delete(headerId); } } /** * Validates and corrects the stream duration based on format initialization metadata. * @param formatInitializationMetadata - The metadata from an initialized format. * @private */ validateAndCorrectDuration(formatInitializationMetadata) { const durationUnits = parseInt(formatInitializationMetadata.durationUnits || "0"); const durationTimescale = parseInt(formatInitializationMetadata.durationTimescale || "0"); if (durationTimescale === 0) { this.logger.warn(TAG, "Invalid timescale (0) in format initialization metadata"); return; } const expectedDuration = Math.trunc(durationUnits / (durationTimescale / 1e3)); if (this.durationMs !== expectedDuration) { this.durationMs = expectedDuration; this.logger.debug(TAG, `Corrected stream duration to ${this.durationMs}ms based on format initialization metadata`); } } /** * Validates downloaded segments for completeness and consistency after the stream finishes. * Checks for duration coverage, missing segments, and duplicates. * @private */ validateDownloadedSegments() { for (const [formatIdKey, initializedFormat] of this.initializedFormatsMap.entries()) { if (formatIdKey === this.formatToDiscard) { this.logger.debug(TAG, `Skipping validation for discarded format: ${formatIdKey}`); continue; } const totalDuration = getTotalDownloadedDuration(initializedFormat); const durationUnits = parseInt(initializedFormat.formatInitializationMetadata.durationUnits || "0"); const durationTimescale = parseInt(initializedFormat.formatInitializationMetadata.durationTimescale || "0"); const expectedDuration = durationTimescale ? durationUnits / (durationTimescale / 1e3) : 0; const durationMismatch = Math.abs(totalDuration - expectedDuration); if (expectedDuration > 0 && durationMismatch > expectedDuration * .01) { const durationCoverage = Math.round(totalDuration / expectedDuration * 100); this.logger.warn(TAG, `Incomplete stream for format ${formatIdKey}: downloaded ${totalDuration}ms (${durationCoverage}%), expected ${expectedDuration}ms`); } const segments = Array.from(initializedFormat.downloadedSegments.entries()); if (segments.length === 0) continue; segments.sort(([numA], [numB]) => numA - numB); const expectedSegmentCount = parseInt(initializedFormat.formatInitializationMetadata.endSegmentNumber || "0"); const missingSegments = []; for (let i = 0; i <= expectedSegmentCount; i++) if (!initializedFormat.downloadedSegments.has(i)) missingSegments.push(i); const uniqueSegmentCount = new Set(segments.map(([num]) => num)).size; const hasDuplicates = uniqueSegmentCount !== segments.length; if (missingSegments.length > 0) { const message = `Format ${formatIdKey}: Missing segments: [${missingSegments.join(", ")}]. Expected range: 0-${expectedSegmentCount}. `; this.logger.warn(TAG, message); this.errorHandler(new Error(message), true); } else this.logger.debug(TAG, `Format ${formatIdKey}: All ${expectedSegmentCount} segments present (100% coverage)`); if (hasDuplicates) { const message = `Format ${formatIdKey}: Found duplicate segment numbers (${segments.length} segments but ${uniqueSegmentCount} unique numbers)`; this.logger.warn(TAG, message); this.errorHandler(new Error(message), true); } } } /** * Resets the internal state of the stream. * Clears all maps, resets counters, and re-initializes the progress tracker. * @private */ resetState() { this.initializedFormatsMap.clear(); this.partialSegmentQueue.clear(); this.activeSabrContextTypes.clear(); this.sabrContexts.clear(); this.nextRequestPolicy = void 0; this.mainFormat = void 0; this.requestNumber = 0; this.cachedBufferedRanges = void 0; this.mediaHeadersProcessed = false; this.streamProtectionStatus = void 0; this.formatToDiscard = void 0; this.abortController = void 0; this.progressTracker = { lastProgressTime: Date.now(), lastDownloadedDuration: 0, stallCount: 0 }; } /** * Handles errors during the streaming process. * @param error - The error that occurred. * @param notifyControllers - Whether to propagate the error to the stream controllers. * @private */ errorHandler(error, notifyControllers = true) { this.resetState(); this.logger.error(TAG, `Stream error: ${error.message}`); if (notifyControllers) { this._errored = true; this.videoController?.error(error); this.audioController?.error(error); } } }; //#endregion //#region src/audioDownloader/utils.ts function normalizeAudioLanguageTag(value) { if (typeof value !== "string") return ""; return value.trim().toLowerCase().replaceAll("_", "-"); } function getYoutubeAudioFormatLanguage(format, isValid) { const accept = (value) => { const tag = normalizeAudioLanguageTag(value); if (!tag) return void 0; if (isValid && !isValid(tag)) return void 0; return tag; }; const direct = accept(format?.languageCode ?? format?.language ?? format?.audioTrack?.languageCode ?? format?.audioTrack?.language); if (direct) return direct; const trackId = format?.audioTrack?.id ?? format?.audioTrackId; if (typeof trackId === "string" && trackId) { const idLanguage = accept(trackId.split(".")[0]); if (idLanguage) return idLanguage; } try { const cipher = typeof format?.signatureCipher === "string" ? new URLSearchParams(format.signatureCipher) : void 0; const rawUrl = format?.url ?? cipher?.get("url"); if (typeof rawUrl === "string") { const xtags = new URL(rawUrl).searchParams.get("xtags") ?? ""; const fromUrl = accept(/(?:^|:)lang=([^:]+)/i.exec(xtags)?.[1]); if (fromUrl) return fromUrl; } } catch {} return ""; } function selectSmallestAudioFormat(candidates) { const smallest = (key) => { let best; let bestValue = Number.POSITIVE_INFINITY; for (const candidate of candidates) { const raw = candidate[key]; const value = raw == null ? NaN : typeof raw === "number" ? raw : Number(String(raw)); if (Number.isFinite(value) && value > 0 && value < bestValue) { best = candidate; bestValue = value; } } return best; }; return smallest("contentLength") ?? smallest("averageBitrate") ?? candidates[0]; } //#endregion //#region src/audioDownloader/strategies/ytPlayerSolver.js var e$1 = Object.defineProperty, t$1 = (t, n) => { let r = {}; for (var i in t) e$1(r, i, { get: t[i], enumerable: !0 }); return n || e$1(r, Symbol.toStringTag, { value: "Module" }), r; }, n$1 = /* @__PURE__ */ t$1({ EXPRESSIONS_PRECEDENCE: () => a$1, GENERATOR: () => ne, NEEDS_PARENTHESES: () => 17, baseGenerator: () => ie, generate: () => oe }), { stringify: r$1 } = JSON; /* c8 ignore if */ if (!String.prototype.repeat) /* c8 ignore next */ throw Error("String.prototype.repeat is undefined, see https://github.com/davidbonnet/astring#installation"); /* c8 ignore if */ if (!String.prototype.endsWith) /* c8 ignore next */ throw Error("String.prototype.endsWith is undefined, see https://github.com/davidbonnet/astring#installation"); var i$1 = { "||": 2, "??": 3, "&&": 4, "|": 5, "^": 6, "&": 7, "==": 8, "!=": 8, "===": 8, "!==": 8, "<": 9, ">": 9, "<=": 9, ">=": 9, in: 9, instanceof: 9, "<<": 10, ">>": 10, ">>>": 10, "+": 11, "-": 11, "*": 12, "%": 12, "/": 12, "**": 13 }; var a$1 = { ArrayExpression: 20, TaggedTemplateExpression: 20, ThisExpression: 20, Identifier: 20, PrivateIdentifier: 20, Literal: 18, TemplateLiteral: 20, Super: 20, SequenceExpression: 20, MemberExpression: 19, ChainExpression: 19, CallExpression: 19, NewExpression: 19, ArrowFunctionExpression: 17, ClassExpression: 17, FunctionExpression: 17, ObjectExpression: 17, UpdateExpression: 16, UnaryExpression: 15, AwaitExpression: 15, BinaryExpression: 14, LogicalExpression: 13, ConditionalExpression: 4, AssignmentExpression: 3, YieldExpression: 2, RestElement: 1 }; function o$1(e, t) { let { generator: n } = e; if (e.write("("), t != null && t.length > 0) { n[t[0].type](t[0], e); let { length: r } = t; for (let i = 1; i < r; i++) { let r = t[i]; e.write(", "), n[r.type](r, e); } } e.write(")"); } function s$1(e, t, n, r) { let a = e.expressionsPrecedence[t.type]; if (a === 17) return !0; let o = e.expressionsPrecedence[n.type]; return a === o ? a !== 13 && a !== 14 ? !1 : t.operator === "**" && n.operator === "**" ? !r : a === 13 && o === 13 && (t.operator === "??" || n.operator === "??") ? !0 : r ? i$1[t.operator] <= i$1[n.operator] : i$1[t.operator] < i$1[n.operator] : !r && a === 15 && o === 14 && n.operator === "**" || a < o; } function c$1(e, t, n, r) { let { generator: i } = e; s$1(e, t, n, r) ? (e.write("("), i[t.type](t, e), e.write(")")) : i[t.type](t, e); } function l$1(e, t, n, r) { let i = t.split("\n"), a = i.length - 1; if (e.write(i[0].trim()), a > 0) { e.write(r); for (let t = 1; t < a; t++) e.write(n + i[t].trim() + r); e.write(n + i[a].trim()); } } function u$1(e, t, n, r) { let { length: i } = t; for (let a = 0; a < i; a++) { let i = t[a]; e.write(n), i.type[0] === "L" ? e.write("// " + i.value.trim() + "\n", i) : (e.write("/*"), l$1(e, i.value, n, r), e.write("*/" + r)); } } function d$1(e) { let t = e; for (; t != null;) { let { type: e } = t; if (e[0] === "C" && e[1] === "a") return !0; if (e[0] === "M" && e[1] === "e" && e[2] === "m") t = t.object; else return !1; } } function f$1(e, t) { let { generator: n } = e, { declarations: r } = t; e.write(t.kind + " "); let { length: i } = r; if (i > 0) { n.VariableDeclarator(r[0], e); for (let t = 1; t < i; t++) e.write(", "), n.VariableDeclarator(r[t], e); } } var p$1; var m$1; var h$1; var g$1; var ee; var te; var ne = { Program(e, t) { let n = t.indent.repeat(t.indentLevel), { lineEnd: r, writeComments: i } = t; i && e.comments != null && u$1(t, e.comments, n, r); let a = e.body, { length: o } = a; for (let e = 0; e < o; e++) { let o = a[e]; i && o.comments != null && u$1(t, o.comments, n, r), t.write(n), this[o.type](o, t), t.write(r); } i && e.trailingComments != null && u$1(t, e.trailingComments, n, r); }, BlockStatement: te = function(e, t) { let n = t.indent.repeat(t.indentLevel++), { lineEnd: r, writeComments: i } = t, a = n + t.indent; t.write("{"); let o = e.body; if (o != null && o.length > 0) { t.write(r), i && e.comments != null && u$1(t, e.comments, a, r); let { length: s } = o; for (let e = 0; e < s; e++) { let n = o[e]; i && n.comments != null && u$1(t, n.comments, a, r), t.write(a), this[n.type](n, t), t.write(r); } t.write(n); } else i && e.comments != null && (t.write(r), u$1(t, e.comments, a, r), t.write(n)); i && e.trailingComments != null && u$1(t, e.trailingComments, a, r), t.write("}"), t.indentLevel--; }, ClassBody: te, StaticBlock(e, t) { t.write("static "), this.BlockStatement(e, t); }, EmptyStatement(e, t) { t.write(";"); }, ExpressionStatement(e, t) { let n = t.expressionsPrecedence[e.expression.type]; n === 17 || n === 3 && e.expression.left.type[0] === "O" ? (t.write("("), this[e.expression.type](e.expression, t), t.write(")")) : this[e.expression.type](e.expression, t), t.write(";"); }, IfStatement(e, t) { t.write("if ("), this[e.test.type](e.test, t), t.write(") "), this[e.consequent.type](e.consequent, t), e.alternate != null && (t.write(" else "), this[e.alternate.type](e.alternate, t)); }, LabeledStatement(e, t) { this[e.label.type](e.label, t), t.write(": "), this[e.body.type](e.body, t); }, BreakStatement(e, t) { t.write("break"), e.label != null && (t.write(" "), this[e.label.type](e.label, t)), t.write(";"); }, ContinueStatement(e, t) { t.write("continue"), e.label != null && (t.write(" "), this[e.label.type](e.label, t)), t.write(";"); }, WithStatement(e, t) { t.write("with ("), this[e.object.type](e.object, t), t.write(") "), this[e.body.type](e.body, t); }, SwitchStatement(e, t) { let n = t.indent.repeat(t.indentLevel++), { lineEnd: r, writeComments: i } = t; t.indentLevel++; let a = n + t.indent, o = a + t.indent; t.write("switch ("), this[e.discriminant.type](e.discriminant, t), t.write(") {" + r); let { cases: s } = e, { length: c } = s; for (let e = 0; e < c; e++) { let n = s[e]; i && n.comments != null && u$1(t, n.comments, a, r), n.test ? (t.write(a + "case "), this[n.test.type](n.test, t), t.write(":" + r)) : t.write(a + "default:" + r); let { consequent: c } = n, { length: l } = c; for (let e = 0; e < l; e++) { let n = c[e]; i && n.comments != null && u$1(t, n.comments, o, r), t.write(o), this[n.type](n, t), t.write(r); } } t.indentLevel -= 2, t.write(n + "}"); }, ReturnStatement(e, t) { t.write("return"), e.argument && (t.write(" "), this[e.argument.type](e.argument, t)), t.write(";"); }, ThrowStatement(e, t) { t.write("throw "), this[e.argument.type](e.argument, t), t.write(";"); }, TryStatement(e, t) { if (t.write("try "), this[e.block.type](e.block, t), e.handler) { let { handler: n } = e; n.param == null ? t.write(" catch ") : (t.write(" catch ("), this[n.param.type](n.param, t), t.write(") ")), this[n.body.type](n.body, t); } e.finalizer && (t.write(" finally "), this[e.finalizer.type](e.finalizer, t)); }, WhileStatement(e, t) { t.write("while ("), this[e.test.type](e.test, t), t.write(") "), this[e.body.type](e.body, t); }, DoWhileStatement(e, t) { t.write("do "), this[e.body.type](e.body, t), t.write(" while ("), this[e.test.type](e.test, t), t.write(");"); }, ForStatement(e, t) { if (t.write("for ("), e.init != null) { let { init: n } = e; n.type[0] === "V" ? f$1(t, n) : this[n.type](n, t); } t.write("; "), e.test && this[e.test.type](e.test, t), t.write("; "), e.update && this[e.update.type](e.update, t), t.write(") "), this[e.body.type](e.body, t); }, ForInStatement: p$1 = function(e, t) { t.write(`for ${e.await ? "await " : ""}(`); let { left: n } = e; n.type[0] === "V" ? f$1(t, n) : this[n.type](n, t), t.write(e.type[3] === "I" ? " in " : " of "), this[e.right.type](e.right, t), t.write(") "), this[e.body.type](e.body, t); }, ForOfStatement: p$1, DebuggerStatement(e, t) { t.write("debugger;", e); }, FunctionDeclaration: m$1 = function(e, t) { t.write((e.async ? "async " : "") + (e.generator ? "function* " : "function ") + (e.id ? e.id.name : ""), e), o$1(t, e.params), t.write(" "), this[e.body.type](e.body, t); }, FunctionExpression: m$1, VariableDeclaration(e, t) { f$1(t, e), t.write(";"); }, VariableDeclarator(e, t) { this[e.id.type](e.id, t), e.init != null && (t.write(" = "), this[e.init.type](e.init, t)); }, ClassDeclaration(e, t) { if (t.write("class " + (e.id ? `${e.id.name} ` : ""), e), e.superClass) { t.write("extends "); let { superClass: n } = e, { type: r } = n, i = t.expressionsPrecedence[r]; (r[0] !== "C" || r[1] !== "l" || r[5] !== "E") && (i === 17 || i < t.expressionsPrecedence.ClassExpression) ? (t.write("("), this[e.superClass.type](n, t), t.write(")")) : this[n.type](n, t), t.write(" "); } this.ClassBody(e.body, t); }, ImportDeclaration(e, t) { t.write("import "); let { specifiers: n, attributes: r } = e, { length: i } = n, a = 0; if (i > 0) { for (; a < i;) { a > 0 && t.write(", "); let e = n[a], r = e.type[6]; if (r === "D") t.write(e.local.name, e), a++; else if (r === "N") t.write("* as " + e.local.name, e), a++; else break; } if (a < i) { for (t.write("{");;) { let e = n[a], { name: r } = e.imported; if (t.write(r, e), r !== e.local.name && t.write(" as " + e.local.name), ++a < i) t.write(", "); else break; } t.write("}"); } t.write(" from "); } if (this.Literal(e.source, t), r && r.length > 0) { t.write(" with { "); for (let e = 0; e < r.length; e++) this.ImportAttribute(r[e], t), e < r.length - 1 && t.write(", "); t.write(" }"); } t.write(";"); }, ImportAttribute(e, t) { this.Identifier(e.key, t), t.write(": "), this.Literal(e.value, t); }, ImportExpression(e, t) { t.write("import("), this[e.source.type](e.source, t), t.write(")"); }, ExportDefaultDeclaration(e, t) { t.write("export default "), this[e.declaration.type](e.declaration, t), t.expressionsPrecedence[e.declaration.type] != null && e.declaration.type[0] !== "F" && t.write(";"); }, ExportNamedDeclaration(e, t) { if (t.write("export "), e.declaration) this[e.declaration.type](e.declaration, t); else { t.write("{"); let { specifiers: n } = e, { length: r } = n; if (r > 0) for (let e = 0;;) { let i = n[e], { name: a } = i.local; if (t.write(a, i), a !== i.exported.name && t.write(" as " + i.exported.name), ++e < r) t.write(", "); else break; } if (t.write("}"), e.source && (t.write(" from "), this.Literal(e.source, t)), e.attributes && e.attributes.length > 0) { t.write(" with { "); for (let n = 0; n < e.attributes.length; n++) this.ImportAttribute(e.attributes[n], t), n < e.attributes.length - 1 && t.write(", "); t.write(" }"); } t.write(";"); } }, ExportAllDeclaration(e, t) { if (e.exported == null ? t.write("export * from ") : t.write("export * as " + e.exported.name + " from "), this.Literal(e.source, t), e.attributes && e.attributes.length > 0) { t.write(" with { "); for (let n = 0; n < e.attributes.length; n++) this.ImportAttribute(e.attributes[n], t), n < e.attributes.length - 1 && t.write(", "); t.write(" }"); } t.write(";"); }, MethodDefinition(e, t) { e.static && t.write("static "); let n = e.kind[0]; (n === "g" || n === "s") && t.write(e.kind + " "), e.value.async && t.write("async "), e.value.generator && t.write("*"), e.computed ? (t.write("["), this[e.key.type](e.key, t), t.write("]")) : this[e.key.type](e.key, t), o$1(t, e.value.params), t.write(" "), this[e.value.body.type](e.value.body, t); }, ClassExpression(e, t) { this.ClassDeclaration(e, t); }, ArrowFunctionExpression(e, t) { t.write(e.async ? "async " : "", e); let { params: n } = e; n != null && (n.length === 1 && n[0].type[0] === "I" ? t.write(n[0].name, n[0]) : o$1(t, e.params)), t.write(" => "), e.body.type[0] === "O" ? (t.write("("), this.ObjectExpression(e.body, t), t.write(")")) : this[e.body.type](e.body, t); }, ThisExpression(e, t) { t.write("this", e); }, Super(e, t) { t.write("super", e); }, RestElement: h$1 = function(e, t) { t.write("..."), this[e.argument.type](e.argument, t); }, SpreadElement: h$1, YieldExpression(e, t) { t.write(e.delegate ? "yield*" : "yield"), e.argument && (t.write(" "), this[e.argument.type](e.argument, t)); }, AwaitExpression(e, t) { t.write("await ", e), c$1(t, e.argument, e); }, TemplateLiteral(e, t) { let { quasis: n, expressions: r } = e; t.write("`"); let { length: i } = r; for (let e = 0; e < i; e++) { let i = r[e], a = n[e]; t.write(a.value.raw, a), t.write("${"), this[i.type](i, t), t.write("}"); } let a = n[n.length - 1]; t.write(a.value.raw, a), t.write("`"); }, TemplateElement(e, t) { t.write(e.value.raw, e); }, TaggedTemplateExpression(e, t) { c$1(t, e.tag, e), this[e.quasi.type](e.quasi, t); }, ArrayExpression: ee = function(e, t) { if (t.write("["), e.elements.length > 0) { let { elements: n } = e, { length: r } = n; for (let e = 0;;) { let i = n[e]; if (i != null && this[i.type](i, t), ++e < r) t.write(", "); else { i ?? t.write(", "); break; } } } t.write("]"); }, ArrayPattern: ee, ObjectExpression(e, t) { let n = t.indent.repeat(t.indentLevel++), { lineEnd: r, writeComments: i } = t, a = n + t.indent; if (t.write("{"), e.properties.length > 0) { t.write(r), i && e.comments != null && u$1(t, e.comments, a, r); let o = "," + r, { properties: s } = e, { length: c } = s; for (let e = 0;;) { let n = s[e]; if (i && n.comments != null && u$1(t, n.comments, a, r), t.write(a), this[n.type](n, t), ++e < c) t.write(o); else break; } t.write(r), i && e.trailingComments != null && u$1(t, e.trailingComments, a, r), t.write(n + "}"); } else i ? e.comments == null ? e.trailingComments == null ? t.write("}") : (t.write(r), u$1(t, e.trailingComments, a, r), t.write(n + "}")) : (t.write(r), u$1(t, e.comments, a, r), e.trailingComments != null && u$1(t, e.trailingComments, a, r), t.write(n + "}")) : t.write("}"); t.indentLevel--; }, Property(e, t) { e.method || e.kind[0] !== "i" ? this.MethodDefinition(e, t) : (e.shorthand || (e.computed ? (t.write("["), this[e.key.type](e.key, t), t.write("]")) : this[e.key.type](e.key, t), t.write(": ")), this[e.value.type](e.value, t)); }, PropertyDefinition(e, t) { if (e.static && t.write("static "), e.computed && t.write("["), this[e.key.type](e.key, t), e.computed && t.write("]"), e.value == null) { e.key.type[0] !== "F" && t.write(";"); return; } t.write(" = "), this[e.value.type](e.value, t), t.write(";"); }, ObjectPattern(e, t) { if (t.write("{"), e.properties.length > 0) { let { properties: n } = e, { length: r } = n; for (let e = 0; this[n[e].type](n[e], t), ++e < r;) t.write(", "); } t.write("}"); }, SequenceExpression(e, t) { o$1(t, e.expressions); }, UnaryExpression(e, t) { if (e.prefix) { let { operator: n, argument: r, argument: { type: i } } = e; t.write(n); let a = s$1(t, r, e); !a && (n.length > 1 || i[0] === "U" && (i[1] === "n" || i[1] === "p") && r.prefix && r.operator[0] === n && (n === "+" || n === "-")) && t.write(" "), a ? (t.write(n.length > 1 ? " (" : "("), this[i](r, t), t.write(")")) : this[i](r, t); } else this[e.argument.type](e.argument, t), t.write(e.operator); }, UpdateExpression(e, t) { e.prefix ? (t.write(e.operator), this[e.argument.type](e.argument, t)) : (this[e.argument.type](e.argument, t), t.write(e.operator)); }, AssignmentExpression(e, t) { this[e.left.type](e.left, t), t.write(" " + e.operator + " "), this[e.right.type](e.right, t); }, AssignmentPattern(e, t) { this[e.left.type](e.left, t), t.write(" = "), this[e.right.type](e.right, t); }, BinaryExpression: g$1 = function(e, t) { let n = e.operator === "in"; n && t.write("("), c$1(t, e.left, e, !1), t.write(" " + e.operator + " "), c$1(t, e.right, e, !0), n && t.write(")"); }, LogicalExpression: g$1, ConditionalExpression(e, t) { let { test: n } = e, r = t.expressionsPrecedence[n.type]; r === 17 || r <= t.expressionsPrecedence.ConditionalExpression ? (t.write("("), this[n.type](n, t), t.write(")")) : this[n.type](n, t), t.write(" ? "), this[e.consequent.type](e.consequent, t), t.write(" : "), this[e.alternate.type](e.alternate, t); }, NewExpression(e, t) { t.write("new "); let n = t.expressionsPrecedence[e.callee.type]; n === 17 || n < t.expressionsPrecedence.CallExpression || d$1(e.callee) ? (t.write("("), this[e.callee.type](e.callee, t), t.write(")")) : this[e.callee.type](e.callee, t), o$1(t, e.arguments); }, CallExpression(e, t) { let n = t.expressionsPrecedence[e.callee.type]; n === 17 || n < t.expressionsPrecedence.CallExpression ? (t.write("("), this[e.callee.type](e.callee, t), t.write(")")) : this[e.callee.type](e.callee, t), e.optional && t.write("?."), o$1(t, e.arguments); }, ChainExpression(e, t) { this[e.expression.type](e.expression, t); }, MemberExpression(e, t) { let n = t.expressionsPrecedence[e.object.type]; n === 17 || n < t.expressionsPrecedence.MemberExpression ? (t.write("("), this[e.object.type](e.object, t), t.write(")")) : this[e.object.type](e.object, t), e.computed ? (e.optional && t.write("?."), t.write("["), this[e.property.type](e.property, t), t.write("]")) : (e.optional ? t.write("?.") : t.write("."), this[e.property.type](e.property, t)); }, MetaProperty(e, t) { t.write(e.meta.name + "." + e.property.name, e); }, Identifier(e, t) { t.write(e.name, e); }, PrivateIdentifier(e, t) { t.write(`#${e.name}`, e); }, Literal(e, t) { e.raw == null ? e.regex == null ? e.bigint == null ? t.write(r$1(e.value), e) : t.write(e.bigint + "n", e) : this.RegExpLiteral(e, t) : t.write(e.raw, e); }, RegExpLiteral(e, t) { let { regex: n } = e; t.write(`/${n.pattern}/${n.flags}`, e); } }; var re = {}; var ie = ne; var ae = class { constructor(e) { let t = e ?? re; this.output = "", t.output == null ? this.output = "" : (this.output = t.output, this.write = this.writeToStream), this.generator = t.generator == null ? ne : t.generator, this.expressionsPrecedence = t.expressionsPrecedence == null ? a$1 : t.expressionsPrecedence, this.indent = t.indent == null ? " " : t.indent, this.lineEnd = t.lineEnd == null ? "\n" : t.lineEnd, this.indentLevel = t.startingIndentLevel == null ? 0 : t.startingIndentLevel, this.writeComments = t.comments ? t.comments : !1, t.sourceMap != null && (this.write = t.output == null ? this.writeAndMap : this.writeToStreamAndMap, this.sourceMap = t.sourceMap, this.line = 1, this.column = 0, this.lineEndSize = this.lineEnd.split("\n").length - 1, this.mapping = { original: null, generated: this, name: void 0, source: t.sourceMap.file || t.sourceMap._file }); } write(e) { this.output += e; } writeToStream(e) { this.output.write(e); } writeAndMap(e, t) { this.output += e, this.map(e, t); } writeToStreamAndMap(e, t) { this.output.write(e), this.map(e, t); } map(e, t) { if (t != null) { let { type: n } = t; if (n[0] === "L" && n[2] === "n") { this.column = 0, this.line++; return; } if (t.loc != null) { let { mapping: e } = this; e.original = t.loc.start, e.name = t.name, this.sourceMap.addMapping(e); } if (n[0] === "T" && n[8] === "E" || n[0] === "L" && n[1] === "i" && typeof t.value == "string") { let { length: t } = e, { column: n, line: r } = this; for (let i = 0; i < t; i++) e[i] === "\n" ? (n = 0, r++) : n++; this.column = n, this.line = r; return; } } let { length: n } = e, { lineEnd: r } = this; n > 0 && (this.lineEndSize > 0 && (r.length === 1 ? e[n - 1] === r : e.endsWith(r)) ? (this.line += this.lineEndSize, this.column = 0) : this.column += n); } toString() { return this.output; } }; function oe(e, t) { let n = new ae(t); return n.generator[e.type](e, n), n.output; } var se = /* @__PURE__ */ t$1({ parse: () => Sr, parseModule: () => xr, parseScript: () => br, version: () => yr }); var ce = ((e, t) => { let n = /* @__PURE__ */ new Uint32Array(69632), r = 0, i = 0; for (; r < 2571;) { let a = e[r++]; if (a < 0) i -= a; else { let o = e[r++]; a & 2 && (o = t[o]), a & 1 ? n.fill(o, i, i += e[r++]) : n[i++] = o; } } return n; })([ -1, 2, 26, 2, 27, 2, 5, -1, 0, 77595648, 3, 44, 2, 3, 0, 14, 2, 63, 2, 64, 3, 0, 3, 0, 3168796671, 0, 4294956992, 2, 1, 2, 0, 2, 41, 3, 0, 4, 0, 4294966523, 3, 0, 4, 2, 16, 2, 65, 2, 0, 0, 4294836735, 0, 3221225471, 0, 4294901942, 2, 66, 0, 134152192, 3, 0, 2, 0, 4294951935, 3, 0, 2, 0, 2683305983, 0, 2684354047, 2, 18, 2, 0, 0, 4294961151, 3, 0, 2, 2, 19, 2, 0, 0, 608174079, 2, 0, 2, 60, 2, 7, 2, 6, 0, 4286611199, 3, 0, 2, 2, 1, 3, 0, 3, 0, 4294901711, 2, 40, 0, 4089839103, 0, 2961209759, 0, 1342439375, 0, 4294543342, 0, 3547201023, 0, 1577204103, 0, 4194240, 0, 4294688750, 2, 2, 0, 80831, 0, 4261478351, 0, 4294549486, 2, 2, 0, 2967484831, 0, 196559, 0, 3594373100, 0, 3288319768, 0, 8469959, 0, 65472, 2, 3, 0, 4093640191, 0, 660618719, 0, 65487, 0, 4294828015, 0, 4092591615, 0, 1616920031, 0, 982991, 2, 3, 2, 0, 0, 2163244511, 0, 4227923919, 0, 4236247022, 2, 71, 0, 4284449919, 0, 851904, 2, 4, 2, 12, 0, 67076095, -1, 2, 72, 0, 1073741743, 0, 4093607775, -1, 0, 50331649, 0, 3265266687, 2, 33, 0, 4294844415, 0, 4278190047, 2, 20, 2, 137, -1, 3, 0, 2, 2, 23, 2, 0, 2, 10, 2, 0, 2, 15, 2, 22, 3, 0, 10, 2, 74, 2, 0, 2, 75, 2, 76, 2, 77, 2, 0, 2, 78, 2, 0, 2, 11, 0, 261632, 2, 25, 3, 0, 2, 2, 13, 2, 4, 3, 0, 18, 2, 79, 2, 5, 3, 0, 2, 2, 80, 0, 2151677951, 2, 29, 2, 9, 0, 909311, 3, 0, 2, 0, 814743551, 2, 49, 0, 67090432, 3, 0, 2, 2, 42, 2, 0, 2, 6, 2, 0, 2, 30, 2, 8, 0, 268374015, 2, 110, 2, 51, 2, 0, 2, 81, 0, 134153215, -1, 2, 7, 2, 0, 2, 8, 0, 2684354559, 0, 67044351, 0, 3221160064, 2, 17, -1, 3, 0, 2, 2, 53, 0, 1046528, 3, 0, 3, 2, 9, 2, 0, 2, 54, 0, 4294960127, 2, 10, 2, 6, 2, 11, 0, 4294377472, 2, 12, 3, 0, 16, 2, 13, 2, 0, 2, 82, 2, 10, 2, 0, 2, 83, 2, 84, 2, 85, 0, 12288, 2, 55, 0, 1048577, 2, 86, 2, 14, -1, 2, 14, 0, 131042, 2, 87, 2, 88, 2, 89, 2, 0, 2, 34, -83, 3, 0, 7, 0, 1046559, 2, 0, 2, 15, 2, 0, 0, 2147516671, 2, 21, 3, 90, 2, 2, 0, -16, 2, 91, 0, 524222462, 2, 4, 2, 0, 0, 4269801471, 2, 4, 3, 0, 2, 2, 28, 2, 16, 3, 0, 2, 2, 17, 2, 0, -1, 2, 18, -16, 3, 0, 206, -2, 3, 0, 692, 2, 73, -1, 2, 18, 2, 10, 3, 0, 8, 2, 93, 2, 133, 2, 0, 0, 3220242431, 3, 0, 3, 2, 19, 2, 94, 2, 95, 3, 0, 2, 2, 96, 2, 0, 2, 97, 2, 46, 2, 0, 0, 4351, 2, 0, 2, 9, 3, 0, 2, 0, 67043391, 0, 3909091327, 2, 0, 2, 24, 2, 9, 2, 20, 3, 0, 2, 0, 67076097, 2, 8, 2, 0, 2, 21, 0, 67059711, 0, 4236247039, 3, 0, 2, 0, 939524103, 0, 8191999, 2, 101, 2, 102, 2, 22, 2, 23, 3, 0, 3, 0, 67057663, 3, 0, 349, 2, 103, 2, 104, 2, 7, -264, 3, 0, 11, 2, 24, 3, 0, 2, 2, 32, -1, 0, 3774349439, 2, 105, 2, 106, 3, 0, 2, 2, 19, 2, 107, 3, 0, 10, 2, 10, 2, 18, 2, 0, 2, 47, 2, 0, 2, 31, 2, 108, 2, 25, 0, 1638399, 0, 57344, 2, 109, 3, 0, 3, 2, 20, 2, 26, 2, 27, 2, 5, 2, 28, 2, 0, 2, 8, 2, 111, -1, 2, 112, 2, 113, 2, 114, -1, 3, 0, 3, 2, 12, -2, 2, 0, 2, 29, -3, 0, 536870912, -4, 2, 20, 2, 0, 2, 36, 0, 1, 2, 0, 2, 67, 2, 6, 2, 12, 2, 10, 2, 0, 2, 115, -1, 3, 0, 4, 2, 10, 2, 23, 2, 116, 2, 7, 2, 0, 2, 117, 2, 0, 2, 118, 2, 119, 2, 120, 2, 0, 2, 9, 3, 0, 9, 2, 21, 2, 30, 2, 31, 2, 121, 2, 122, -2, 2, 123, 2, 124, 2, 30, 2, 21, 2, 8, -2, 2, 125, 2, 30, 2, 32, -2, 2, 0, 2, 39, -2, 0, 4277137519, 0, 2269118463, -1, 3, 20, 2, -1, 2, 33, 2, 38, 2, 0, 3, 30, 2, 2, 35, 2, 19, -3, 3, 0, 2, 2, 34, -1, 2, 0, 2, 35, 2, 0, 2, 35, 2, 0, 2, 48, 2, 0, 0, 4294950463, 2, 37, -7, 2, 0, 0, 203775, 2, 57, 0, 4026531840, 2, 20, 2, 43, 2, 36, 2, 18, 2, 37, 2, 18, 2, 126, 2, 21, 3, 0, 2, 2, 38, 0, 2151677888, 2, 0, 2, 12, 0, 4294901764, 2, 144, 2, 0, 2, 58, 2, 56, 0, 5242879, 3, 0, 2, 0, 402644511, -1, 2, 128, 2, 39, 0, 3, -1, 2, 129, 2, 130, 2, 0, 0, 67045375, 2, 40, 0, 4226678271, 0, 3766565279, 0, 2039759, 2, 132, 2, 41, 0, 1046437, 0, 6, 3, 0, 2, 0, 3288270847, 0, 3, 3, 0, 2, 0, 67043519, -5, 2, 0, 0, 4282384383, 0, 1056964609, -1, 3, 0, 2, 0, 67043345, -1, 2, 0, 2, 42, 2, 23, 2, 50, 2, 11, 2, 61, 2, 38, -5, 2, 0, 2, 12, -3, 3, 0, 2, 0, 2147484671, 2, 134, 0, 4190109695, 2, 52, -2, 2, 135, 0, 4244635647, 0, 27, 2, 0, 2, 8, 2, 43, 2, 0, 2, 68, 2, 18, 2, 0, 2, 42, -6, 2, 0, 2, 45, 2, 59, 2, 44, 2, 45, 2, 46, 2, 47, 0, 8388351, -2, 2, 136, 0, 3028287487, 2, 48, 2, 138, 0, 33259519, 2, 49, -9, 2, 21, 0, 4294836223, 0, 3355443199, 0, 134152199, -2, 2, 69, -2, 3, 0, 28, 2, 32, -3, 3, 0, 3, 2, 17, 3, 0, 6, 2, 50, -81, 2, 18, 3, 0, 2, 2, 36, 3, 0, 33, 2, 25, 2, 30, 3, 0, 124, 2, 12, 3, 0, 18, 2, 38, -213, 2, 0, 2, 32, -54, 3, 0, 17, 2, 42, 2, 8, 2, 23, 2, 0, 2, 8, 2, 23, 2, 51, 2, 0, 2, 21, 2, 52, 2, 139, 2, 25, -13, 2, 0, 2, 53, -6, 3, 0, 2, -4, 3, 0, 2, 0, 4294936575, 2, 0, 0, 4294934783, -2, 0, 196635, 3, 0, 191, 2, 54, 3, 0, 38, 2, 30, 2, 55, 2, 34, -278, 2, 140, 3, 0, 9, 2, 141, 2, 142, 2, 56, 3, 0, 11, 2, 7, -72, 3, 0, 3, 2, 143, 0, 1677656575, -130, 2, 26, -16, 2, 0, 2, 24, 2, 38, -16, 0, 4161266656, 0, 4071, 0, 15360, -4, 2, 57, -13, 3, 0, 2, 2, 58, 2, 0, 2, 145, 2, 146, 2, 62, 2, 0, 2, 147, 2, 148, 2, 149, 3, 0, 10, 2, 150, 2, 151, 2, 22, 3, 58, 2, 3, 152, 2, 3, 59, 2, 0, 4294954999, 2, 0, -16, 2, 0, 2, 92, 2, 0, 0, 2105343, 0, 4160749584, 0, 65534, -34, 2, 8, 2, 154, -6, 0, 4194303871, 0, 4294903771, 2, 0, 2, 60, 2, 100, -3, 2, 0, 0, 1073684479, 0, 17407, -9, 2, 18, 2, 17, 2, 0, 2, 32, -14, 2, 18, 2, 32, -6, 2, 18, 2, 12, -15, 2, 155, 3, 0, 6, 0, 8323103, -1, 3, 0, 2, 2, 61, -37, 2, 62, 2, 156, 2, 157, 2, 158, 2, 159, 2, 160, -105, 2, 26, -32, 3, 0, 1335, -1, 3, 0, 129, 2, 32, 3, 0, 6, 2, 10, 3, 0, 180, 2, 161, 3, 0, 233, 2, 162, 3, 0, 18, 2, 10, -77, 3, 0, 16, 2, 10, -47, 3, 0, 154, 2, 6, 3, 0, 130, 2, 25, -22250, 3, 0, 7, 2, 25, -6130, 3, 5, 2, -1, 0, 69207040, 3, 44, 2, 3, 0, 14, 2, 63, 2, 64, -3, 0, 3168731136, 0, 4294956864, 2, 1, 2, 0, 2, 41, 3, 0, 4, 0, 4294966275, 3, 0, 4, 2, 16, 2, 65, 2, 0, 2, 34, -1, 2, 18, 2, 66, -1, 2, 0, 0, 2047, 0, 4294885376, 3, 0, 2, 0, 3145727, 0, 2617294944, 0, 4294770688, 2, 25, 2, 67, 3, 0, 2, 0, 131135, 2, 98, 0, 70256639, 0, 71303167, 0, 272, 2, 42, 2, 6, 0, 32511, 2, 0, 2, 49, -1, 2, 99, 2, 68, 0, 4278255616, 0, 4294836227, 0, 4294549473, 0, 600178175, 0, 2952806400, 0, 268632067, 0, 4294543328, 0, 57540095, 0, 1577058304, 0, 1835008, 0, 4294688736, 2, 70, 2, 69, 0, 33554435, 2, 131, 2, 70, 0, 2952790016, 0, 131075, 0, 3594373096, 0, 67094296, 2, 69, -1, 0, 4294828e3, 0, 603979263, 0, 654311424, 0, 3, 0, 4294828001, 0, 602930687, 0, 1610612736, 0, 393219, 0, 4294828016, 0, 671088639, 0, 2154840064, 0, 4227858435, 0, 4236247008, 2, 71, 2, 38, -1, 2, 4, 0, 917503, 2, 38, -1, 2, 72, 0, 537788335, 0, 4026531935, -1, 0, 1, -1, 2, 33, 2, 73, 0, 7936, -3, 2, 0, 0, 2147485695, 0, 1010761728, 0, 4292984930, 0, 16387, 2, 0, 2, 15, 2, 22, 3, 0, 10, 2, 74, 2, 0, 2, 75, 2, 76, 2, 77, 2, 0, 2, 78, 2, 0, 2, 12, -1, 2, 25, 3, 0, 2, 2, 13, 2, 4, 3, 0, 18, 2, 79, 2, 5, 3, 0, 2, 2, 80, 0, 2147745791, 3, 19, 2, 0, 122879, 2, 0, 2, 9, 0, 276824064, -2, 3, 0, 2, 2, 42, 2, 0, 0, 4294903295, 2, 0, 2, 30, 2, 8, -1, 2, 18, 2, 51, 2, 0, 2, 81, 2, 49, -1, 2, 21, 2, 0, 2, 29, -2, 0, 128, -2, 2, 28, 2, 9, 0, 8160, -1, 2, 127, 0, 4227907585, 2, 0, 2, 37, 2, 0, 2, 50, 0, 4227915776, 2, 10, 2, 6, 2, 11, -1, 0, 74440192, 3, 0, 6, -2, 3, 0, 8, 2, 13, 2, 0, 2, 82, 2, 10, 2, 0, 2, 83, 2, 84, 2, 85, -3, 2, 86, 2, 14, -3, 2, 87, 2, 88, 2, 89, 2, 0, 2, 34, -83, 3, 0, 7, 0, 817183, 2, 0, 2, 15, 2, 0, 0, 33023, 2, 21, 3, 90, 2, -17, 2, 91, 0, 524157950, 2, 4, 2, 0, 2, 92, 2, 4, 2, 0, 2, 22, 2, 28, 2, 16, 3, 0, 2, 2, 17, 2, 0, -1, 2, 18, -16, 3, 0, 206, -2, 3, 0, 692, 2, 73, -1, 2, 18, 2, 10, 3, 0, 8, 2, 93, 0, 3072, 2, 0, 0, 2147516415, 2, 10, 3, 0, 2, 2, 25, 2, 94, 2, 95, 3, 0, 2, 2, 96, 2, 0, 2, 97, 2, 46, 0, 4294965179, 0, 7, 2, 0, 2, 9, 2, 95, 2, 9, -1, 0, 1761345536, 2, 98, 0, 4294901823, 2, 38, 2, 20, 2, 99, 2, 35, 2, 100, 0, 2080440287, 2, 0, 2, 34, 2, 153, 0, 3296722943, 2, 0, 0, 1046675455, 0, 939524101, 0, 1837055, 2, 101, 2, 102, 2, 22, 2, 23, 3, 0, 3, 0, 7, 3, 0, 349, 2, 103, 2, 104, 2, 7, -264, 3, 0, 11, 2, 24, 3, 0, 2, 2, 32, -1, 0, 2700607615, 2, 105, 2, 106, 3, 0, 2, 2, 19, 2, 107, 3, 0, 10, 2, 10, 2, 18, 2, 0, 2, 47, 2, 0, 2, 31, 2, 108, -3, 2, 109, 3, 0, 3, 2, 20, -1, 3, 5, 2, 2, 110, 2, 0, 2, 8, 2, 111, -1, 2, 112, 2, 113, 2, 114, -1, 3, 0, 3, 2, 12, -2, 2, 0, 2, 29, -8, 2, 20, 2, 0, 2, 36, -1, 2, 0, 2, 67, 2, 6, 2, 30, 2, 10, 2, 0, 2, 115, -1, 3, 0, 4, 2, 10, 2, 18, 2, 116, 2, 7, 2, 0, 2, 117, 2, 0, 2, 118, 2, 119, 2, 120, 2, 0, 2, 9, 3, 0, 9, 2, 21, 2, 30, 2, 31, 2, 121, 2, 122, -2, 2, 123, 2, 124, 2, 30, 2, 21, 2, 8, -2, 2, 125, 2, 30, 2, 32, -2, 2, 0, 2, 39, -2, 0, 4277075969, 2, 30, -1, 3, 20, 2, -1, 2, 33, 2, 126, 2, 0, 3, 30, 2, 2, 35, 2, 19, -3, 3, 0, 2, 2, 34, -1, 2, 0, 2, 35, 2, 0, 2, 35, 2, 0, 2, 50, 2, 98, 0, 4294934591, 2, 37, -7, 2, 0, 0, 197631, 2, 57, -1, 2, 20, 2, 43, 2, 37, 2, 18, 0, 3, 2, 18, 2, 126, 2, 21, 2, 127, 2, 54, -1, 0, 2490368, 2, 127, 2, 25, 2, 18, 2, 34, 2, 127, 2, 38, 0, 4294901904, 0, 4718591, 2, 127, 2, 35, 0, 335544350, -1, 2, 128, 0, 2147487743, 0, 1, -1, 2, 129, 2, 130, 2, 8, -1, 2, 131, 2, 70, 0, 3758161920, 0, 3, 2, 132, 0, 12582911, 0, 655360, -1, 2, 0, 2, 29, 0, 2147485568, 0, 3, 2, 0, 2, 25, 0, 176, -5, 2, 0, 2, 17, 0, 251658240, -1, 2, 0, 2, 25, 0, 16, -1, 2, 0, 0, 16779263, -2, 2, 12, -1, 2, 38, -5, 2, 0, 2, 133, -3, 3, 0, 2, 2, 55, 2, 134, 0, 2147549183, 0, 2, -2, 2, 135, 2, 36, 0, 10, 0, 4294965249, 0, 67633151, 0, 4026597376, 2, 0, 0, 536871935, 2, 18, 2, 0, 2, 42, -6, 2, 0, 0, 1, 2, 59, 2, 17, 0, 1, 2, 46, 2, 25, -3, 2, 136, 2, 36, 2, 137, 2, 138, 0, 16778239, -10, 2, 35, 0, 4294836212, 2, 9, -3, 2, 69, -2, 3, 0, 28, 2, 32, -3, 3, 0, 3, 2, 17, 3, 0, 6, 2, 50, -81, 2, 18, 3, 0, 2, 2, 36, 3, 0, 33, 2, 25, 0, 126, 3, 0, 124, 2, 12, 3, 0, 18, 2, 38, -213, 2, 10, -55, 3, 0, 17, 2, 42, 2, 8, 2, 18, 2, 0, 2, 8, 2, 18, 2, 60, 2, 0, 2, 25, 2, 50, 2, 139, 2, 25, -13, 2, 0, 2, 73, -6, 3, 0, 2, -4, 3, 0, 2, 0, 67583, -1, 2, 107, -2, 0, 11, 3, 0, 191, 2, 54, 3, 0, 38, 2, 30, 2, 55, 2, 34, -278, 2, 140, 3, 0, 9, 2, 141, 2, 142, 2, 56, 3, 0, 11, 2, 7, -72, 3, 0, 3, 2, 143, 2, 144, -187, 3, 0, 2, 2, 58, 2, 0, 2, 145, 2, 146, 2, 62, 2, 0, 2, 147, 2, 148, 2, 149, 3, 0, 10, 2, 150, 2, 151, 2, 22, 3, 58, 2, 3, 152, 2, 3, 59, 2, 2, 153, -57, 2, 8, 2, 154, -7, 2, 18, 2, 0, 2, 60, -4, 2, 0, 0, 1065361407, 0, 16384, -9, 2, 18, 2, 60, 2, 0, 2, 133, -14, 2, 18, 2, 133, -6, 2, 18, 0, 81919, -15, 2, 155, 3, 0, 6, 2, 126, -1, 3, 0, 2, 0, 2063, -37, 2, 62, 2, 156, 2, 157, 2, 158, 2, 159, 2, 160, -138, 3, 0, 1335, -1, 3, 0, 129, 2, 32, 3, 0, 6, 2, 10, 3, 0, 180, 2, 161, 3, 0, 233, 2, 162, 3, 0, 18, 2, 10, -77, 3, 0, 16, 2, 10, -47, 3, 0, 154, 2, 6, 3, 0, 130, 2, 25, -28386 ], [ 4294967295, 4294967291, 4092460543, 4294828031, 4294967294, 134217726, 4294903807, 268435455, 2147483647, 1048575, 1073741823, 3892314111, 134217727, 1061158911, 536805376, 4294910143, 4294901759, 32767, 4294901760, 262143, 536870911, 8388607, 4160749567, 4294902783, 4294918143, 65535, 67043328, 2281701374, 4294967264, 2097151, 4194303, 255, 67108863, 4294967039, 511, 524287, 131071, 63, 127, 3238002687, 4294549487, 4290772991, 33554431, 4294901888, 4286578687, 67043329, 4294705152, 4294770687, 67043583, 1023, 15, 2047999, 67043343, 67051519, 16777215, 2147483648, 4294902e3, 28, 4292870143, 4294966783, 16383, 67047423, 4294967279, 262083, 20511, 41943039, 493567, 4294959104, 603979775, 65536, 602799615, 805044223, 4294965206, 8191, 1031749119, 4294917631, 2134769663, 4286578493, 4282253311, 4294942719, 33540095, 4294905855, 2868854591, 1608515583, 265232348, 534519807, 2147614720, 1060109444, 4093640016, 17376, 2139062143, 224, 4169138175, 4294909951, 4286578688, 4294967292, 4294965759, 535511039, 4294966272, 4294967280, 32768, 8289918, 4294934399, 4294901775, 4294965375, 1602223615, 4294967259, 4294443008, 268369920, 4292804608, 4294967232, 486341884, 4294963199, 3087007615, 1073692671, 4128527, 4279238655, 4294902015, 4160684047, 4290246655, 469499899, 4294967231, 134086655, 4294966591, 2445279231, 3670015, 31, 4294967288, 4294705151, 3221208447, 4294902271, 4294549472, 4294921215, 4095, 4285526655, 4294966527, 4294966143, 64, 4294966719, 3774873592, 1877934080, 262151, 2555904, 536807423, 67043839, 3758096383, 3959414372, 3755993023, 2080374783, 4294835295, 4294967103, 4160749565, 4294934527, 4087, 2016, 2147446655, 184024726, 2862017156, 1593309078, 268434431, 268434414, 4294901763, 4294901761 ]); var le = (e) => !!(ce[(e >>> 5) + 0] >>> e & 1); var ue = (e) => !!(ce[(e >>> 5) + 34816] >>> e & 1); function _$1(e) { return e.column++, e.currentChar = e.source.charCodeAt(++e.index); } function de(e) { let t = e.currentChar; if ((t & 64512) != 55296) return 0; let n = e.source.charCodeAt(e.index + 1); return (n & 64512) == 56320 ? 65536 + ((t & 1023) << 10) + (n & 1023) : 0; } function fe(e, t) { e.currentChar = e.source.charCodeAt(++e.index), e.flags |= 1, t & 4 || (e.column = 0, e.line++); } function v$1(e) { e.flags |= 1, e.currentChar = e.source.charCodeAt(++e.index), e.column = 0, e.line++; } function pe(e) { return e === 160 || e === 65279 || e === 133 || e === 5760 || e >= 8192 && e <= 8203 || e === 8239 || e === 8287 || e === 12288 || e === 8201 || e === 65519; } function y$1(e) { return e < 65 ? e - 48 : e - 65 + 10 & 15; } function me(e) { switch (e) { case 134283266: return "NumericLiteral"; case 134283267: return "StringLiteral"; case 86021: case 86022: return "BooleanLiteral"; case 86023: return "NullLiteral"; case 65540: return "RegularExpression"; case 67174408: case 67174409: case 131: return "TemplateLiteral"; default: return (e & 143360) == 143360 ? "Identifier" : (e & 4096) == 4096 ? "Keyword" : "Punctuator"; } } var b$1 = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1032, 0, 0, 2056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8192, 0, 3, 0, 0, 8192, 0, 0, 0, 256, 0, 33024, 0, 0, 242, 242, 114, 114, 114, 114, 114, 114, 594, 594, 0, 0, 16384, 0, 0, 0, 0, 67, 67, 67, 67, 67, 67, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 1, 0, 0, 4099, 0, 71, 71, 71, 71, 71, 71, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 16384, 0, 0, 0, 0 ]; var he = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ]; var ge = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 ]; function _e(e) { return e <= 127 ? he[e] > 0 : ue(e); } function ve(e) { return e <= 127 ? ge[e] > 0 : le(e) || e === 8204 || e === 8205; } var ye = [ "SingleLine", "MultiLine", "HTMLOpen", "HTMLClose", "HashbangComment" ]; function be(e) { let { source: t } = e; e.currentChar === 35 && t.charCodeAt(e.index + 1) === 33 && (_$1(e), _$1(e), Se(e, t, 0, 4, e.tokenStart)); } function xe(e, t, n, r, i, a) { return r & 2 && e.report(0), Se(e, t, n, i, a); } function Se(e, t, n, r, i) { let { index: a } = e; for (e.tokenIndex = e.index, e.tokenLine = e.line, e.tokenColumn = e.column; e.index < e.end;) { if (b$1[e.currentChar] & 8) { let n = e.currentChar === 13; v$1(e), n && e.index < e.end && e.currentChar === 10 && (e.currentChar = t.charCodeAt(++e.index)); break; } if ((e.currentChar ^ 8232) <= 1) { v$1(e); break; } _$1(e), e.tokenIndex = e.index, e.tokenLine = e.line, e.tokenColumn = e.column; } if (e.options.onComment) { let n = { start: { line: i.line, column: i.column }, end: { line: e.tokenLine, column: e.tokenColumn } }; e.options.onComment(ye[r & 255], t.slice(a, e.tokenIndex), i.index, e.tokenIndex, n); } return n | 1; } function Ce(e, t, n) { let { index: r } = e; for (; e.index < e.end;) if (e.currentChar < 43) { let i = !1; for (; e.currentChar === 42;) if (i ||= (n &= -5, !0), _$1(e) === 47) { if (_$1(e), e.options.onComment) { let n = { start: { line: e.tokenLine, column: e.tokenColumn }, end: { line: e.line, column: e.column } }; e.options.onComment(ye[1], t.slice(r, e.index - 2), r - 2, e.index, n); } return e.tokenIndex = e.index, e.tokenLine = e.line, e.tokenColumn = e.column, n; } if (i) continue; b$1[e.currentChar] & 8 ? e.currentChar === 13 ? (n |= 5, v$1(e)) : (fe(e, n), n = n & -5 | 1) : _$1(e); } else (e.currentChar ^ 8232) <= 1 ? (n = n & -5 | 1, v$1(e)) : (n &= -5, _$1(e)); e.report(18); } var x$1; (function(e) { e[e.Empty = 0] = "Empty", e[e.Escape = 1] = "Escape", e[e.Class = 2] = "Class"; })(x$1 ||= {}); var S$1; (function(e) { e[e.Empty = 0] = "Empty", e[e.IgnoreCase = 1] = "IgnoreCase", e[e.Global = 2] = "Global", e[e.Multiline = 4] = "Multiline", e[e.Unicode = 16] = "Unicode", e[e.Sticky = 8] = "Sticky", e[e.DotAll = 32] = "DotAll", e[e.Indices = 64] = "Indices", e[e.UnicodeSets = 128] = "UnicodeSets"; })(S$1 ||= {}); function we(e) { let t = e.index, n = x$1.Empty; loop: for (;;) { let t = e.currentChar; if (_$1(e), n & x$1.Escape) n &= ~x$1.Escape; else switch (t) { case 47: if (n) break; break loop; case 92: n |= x$1.Escape; break; case 91: n |= x$1.Class; break; case 93: n &= x$1.Escape; } if ((t === 13 || t === 10 || t === 8232 || t === 8233) && e.report(34), e.index >= e.source.length) return e.report(34); } let r = e.index - 1, i = S$1.Empty, a = e.currentChar, { index: o } = e; for (; ve(a);) { switch (a) { case 103: i & S$1.Global && e.report(36, "g"), i |= S$1.Global; break; case 105: i & S$1.IgnoreCase && e.report(36, "i"), i |= S$1.IgnoreCase; break; case 109: i & S$1.Multiline && e.report(36, "m"), i |= S$1.Multiline; break; case 117: i & S$1.Unicode && e.report(36, "u"), i & S$1.UnicodeSets && e.report(36, "vu"), i |= S$1.Unicode; break; case 118: i & S$1.Unicode && e.report(36, "uv"), i & S$1.UnicodeSets && e.report(36, "v"), i |= S$1.UnicodeSets; break; case 121: i & S$1.Sticky && e.report(36, "y"), i |= S$1.Sticky; break; case 115: i & S$1.DotAll && e.report(36, "s"), i |= S$1.DotAll; break; case 100: i & S$1.Indices && e.report(36, "d"), i |= S$1.Indices; break; default: e.report(35); } a = _$1(e); } let s = e.source.slice(o, e.index), c = e.source.slice(t, r); return e.tokenRegExp = { pattern: c, flags: s }, e.options.raw && (e.tokenRaw = e.source.slice(e.tokenIndex, e.index)), e.tokenValue = Te(e, c, s), 65540; } function Te(e, t, n) { try { return new RegExp(t, n); } catch { try { return new RegExp(t, n), null; } catch { e.report(34); } } } function Ee(e, t, n) { let { index: r } = e, i = "", a = _$1(e), o = e.index; for (; !(b$1[a] & 8);) { if (a === n) return i += e.source.slice(o, e.index), _$1(e), e.options.raw && (e.tokenRaw = e.source.slice(r, e.index)), e.tokenValue = i, 134283267; if ((a & 8) == 8 && a === 92) { if (i += e.source.slice(o, e.index), a = _$1(e), a < 127 || a === 8232 || a === 8233) { let n = De(e, t, a); n >= 0 ? i += String.fromCodePoint(n) : Oe(e, n, 0); } else i += String.fromCodePoint(a); o = e.index + 1; } else (a === 8232 || a === 8233) && (e.column = -1, e.line++); e.index >= e.end && e.report(16), a = _$1(e); } e.report(16); } function De(e, t, n, r = 0) { switch (n) { case 98: return 8; case 102: return 12; case 114: return 13; case 110: return 10; case 116: return 9; case 118: return 11; case 13: if (e.index < e.end) { let t = e.source.charCodeAt(e.index + 1); t === 10 && (e.index += 1, e.currentChar = t); } case 10: case 8232: case 8233: return e.column = -1, e.line++, -1; case 48: case 49: case 50: case 51: { let i = n - 48, a = e.index + 1, o = e.column + 1; if (a < e.end) { let n = e.source.charCodeAt(a); if (!(b$1[n] & 32)) { if (i !== 0 || b$1[n] & 512) { if (t & 1 || r) return -2; e.flags |= 64; } } else if (t & 1 || r) return -2; else { if (e.currentChar = n, i = i << 3 | n - 48, a++, o++, a < e.end) { let t = e.source.charCodeAt(a); b$1[t] & 32 && (e.currentChar = t, i = i << 3 | t - 48, a++, o++); } e.flags |= 64; } e.index = a - 1, e.column = o - 1; } return i; } case 52: case 53: case 54: case 55: { if (r || t & 1) return -2; let i = n - 48, a = e.index + 1, o = e.column + 1; if (a < e.end) { let t = e.source.charCodeAt(a); b$1[t] & 32 && (i = i << 3 | t - 48, e.currentChar = t, e.index = a, e.column = o); } return e.flags |= 64, i; } case 120: { let t = _$1(e); if (!(b$1[t] & 64)) return -4; let n = y$1(t), r = _$1(e); if (!(b$1[r] & 64)) return -4; let i = y$1(r); return n << 4 | i; } case 117: { let t = _$1(e); if (e.currentChar === 123) { let t = 0; for (; b$1[_$1(e)] & 64;) if (t = t << 4 | y$1(e.currentChar), t > 1114111) return -5; return e.currentChar < 1 || e.currentChar !== 125 ? -4 : t; } { if (!(b$1[t] & 64)) return -4; let n = e.source.charCodeAt(e.index + 1); if (!(b$1[n] & 64)) return -4; let r = e.source.charCodeAt(e.index + 2); if (!(b$1[r] & 64)) return -4; let i = e.source.charCodeAt(e.index + 3); return b$1[i] & 64 ? (e.index += 3, e.column += 3, e.currentChar = e.source.charCodeAt(e.index), y$1(t) << 12 | y$1(n) << 8 | y$1(r) << 4 | y$1(i)) : -4; } } case 56: case 57: if (r || !e.options.webcompat || t & 1) return -3; e.flags |= 4096; default: return n; } } function Oe(e, t, n) { switch (t) { case -1: return; case -2: e.report(n ? 2 : 1); case -3: e.report(n ? 3 : 14); case -4: e.report(7); case -5: e.report(104); } } function ke(e, t) { let { index: n } = e, r = 67174409, i = "", a = _$1(e); for (; a !== 96;) { if (a === 36 && e.source.charCodeAt(e.index + 1) === 123) { _$1(e), r = 67174408; break; } if (a === 92) if (a = _$1(e), a > 126) i += String.fromCodePoint(a); else { let { index: n, line: o, column: s } = e, c = De(e, t | 1, a, 1); if (c >= 0) i += String.fromCodePoint(c); else if (c !== -1 && t & 64) { e.index = n, e.line = o, e.column = s, i = null, a = Ae(e, a), a < 0 && (r = 67174408); break; } else Oe(e, c, 1); } else e.index < e.end && (a === 13 && e.source.charCodeAt(e.index) === 10 && (i += String.fromCodePoint(a), e.currentChar = e.source.charCodeAt(++e.index)), ((a & 83) < 3 && a === 10 || (a ^ 8232) <= 1) && (e.column = -1, e.line++), i += String.fromCodePoint(a)); e.index >= e.end && e.report(17), a = _$1(e); } return _$1(e), e.tokenValue = i, e.tokenRaw = e.source.slice(n + 1, e.index - (r === 67174409 ? 1 : 2)), r; } function Ae(e, t) { for (; t !== 96;) { switch (t) { case 36: { let n = e.index + 1; if (n < e.end && e.source.charCodeAt(n) === 123) return e.index = n, e.column++, -t; break; } case 10: case 8232: case 8233: e.column = -1, e.line++; } e.index >= e.end && e.report(17), t = _$1(e); } return t; } function je(e, t) { return e.index >= e.end && e.report(0), e.index--, e.column--, ke(e, t); } var Me = { 0: "Unexpected token", 30: "Unexpected token: '%0'", 1: "Octal escape sequences are not allowed in strict mode", 2: "Octal escape sequences are not allowed in template strings", 3: "\\8 and \\9 are not allowed in template strings", 4: "Private identifier #%0 is not defined", 5: "Illegal Unicode escape sequence", 6: "Invalid code point %0", 7: "Invalid hexadecimal escape sequence", 9: "Octal literals are not allowed in strict mode", 8: "Decimal integer literals with a leading zero are forbidden in strict mode", 10: "Expected number in radix %0", 151: "Invalid left-hand side assignment to a destructible right-hand side", 11: "Non-number found after exponent indicator", 12: "Invalid BigIntLiteral", 13: "No identifiers allowed directly after numeric literal", 14: "Escapes \\8 or \\9 are not syntactically valid escapes", 15: "Escapes \\8 or \\9 are not allowed in strict mode", 16: "Unterminated string literal", 17: "Unterminated template literal", 18: "Multiline comment was not closed properly", 19: "The identifier contained dynamic unicode escape that was not closed", 20: "Illegal character '%0'", 21: "Missing hexadecimal digits", 22: "Invalid implicit octal", 23: "Invalid line break in string literal", 24: "Only unicode escapes are legal in identifier names", 25: "Expected '%0'", 26: "Invalid left-hand side in assignment", 27: "Invalid left-hand side in async arrow", 28: "Calls to super must be in the \"constructor\" method of a class expression or class declaration that has a superclass", 29: "Member access on super must be in a method", 31: "Await expression not allowed in formal parameter", 32: "Yield expression not allowed in formal parameter", 95: "Unexpected token: 'escaped keyword'", 33: "Unary expressions as the left operand of an exponentiation expression must be disambiguated with parentheses", 123: "Async functions can only be declared at the top level or inside a block", 34: "Unterminated regular expression", 35: "Unexpected regular expression flag", 36: "Duplicate regular expression flag '%0'", 37: "%0 functions must have exactly %1 argument%2", 38: "Setter function argument must not be a rest parameter", 39: "%0 declaration must have a name in this context", 40: "Function name may not contain any reserved words or be eval or arguments in strict mode", 41: "The rest operator is missing an argument", 42: "A getter cannot be a generator", 43: "A setter cannot be a generator", 44: "A computed property name must be followed by a colon or paren", 134: "Object literal keys that are strings or numbers must be a method or have a colon", 46: "Found `* async x(){}` but this should be `async * x(){}`", 45: "Getters and setters can not be generators", 47: "'%0' can not be generator method", 48: "No line break is allowed after '=>'", 49: "The left-hand side of the arrow can only be destructed through assignment", 50: "The binding declaration is not destructible", 51: "Async arrow can not be followed by new expression", 52: "Classes may not have a static property named 'prototype'", 53: "Class constructor may not be a %0", 54: "Duplicate constructor method in class", 55: "Invalid increment/decrement operand", 56: "Invalid use of `new` keyword on an increment/decrement expression", 57: "`=>` is an invalid assignment target", 58: "Rest element may not have a trailing comma", 59: "Missing initializer in %0 declaration", 60: "'for-%0' loop head declarations can not have an initializer", 61: "Invalid left-hand side in for-%0 loop: Must have a single binding", 62: "Invalid shorthand property initializer", 63: "Property name __proto__ appears more than once in object literal", 64: "Let is disallowed as a lexically bound name", 65: "Invalid use of '%0' inside new expression", 66: "Illegal 'use strict' directive in function with non-simple parameter list", 67: "Identifier \"let\" disallowed as left-hand side expression in strict mode", 68: "Illegal continue statement", 69: "Illegal break statement", 70: "Cannot have `let[...]` as a var name in strict mode", 71: "Invalid destructuring assignment target", 72: "Rest parameter may not have a default initializer", 73: "The rest argument must the be last parameter", 74: "Invalid rest argument", 76: "In strict mode code, functions can only be declared at top level or inside a block", 77: "In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement", 78: "Without web compatibility enabled functions can not be declared at top level, inside a block, or as the body of an if statement", 79: "Class declaration can't appear in single-statement context", 80: "Invalid left-hand side in for-%0", 81: "Invalid assignment in for-%0", 82: "for await (... of ...) is only valid in async functions and async generators", 83: "The first token after the template expression should be a continuation of the template", 85: "`let` declaration not allowed here and `let` cannot be a regular var name in strict mode", 84: "`let \n [` is a restricted production at the start of a statement", 86: "Catch clause requires exactly one parameter, not more (and no trailing comma)", 87: "Catch clause parameter does not support default values", 88: "Missing catch or finally after try", 89: "More than one default clause in switch statement", 90: "Illegal newline after throw", 91: "Strict mode code may not include a with statement", 92: "Illegal return statement", 93: "The left hand side of the for-header binding declaration is not destructible", 94: "new.target only allowed within functions or static blocks", 96: "'#' not followed by identifier", 102: "Invalid keyword", 101: "Can not use 'let' as a class name", 100: "'A lexical declaration can't define a 'let' binding", 99: "Can not use `let` as variable name in strict mode", 97: "'%0' may not be used as an identifier in this context", 98: "Await is only valid in async functions", 103: "The %0 keyword can only be used with the module goal", 104: "Unicode codepoint must not be greater than 0x10FFFF", 105: "%0 source must be string", 106: "Only a identifier or string can be used to indicate alias", 107: "Only '*' or '{...}' can be imported after default", 108: "Trailing decorator may be followed by method", 109: "Decorators can't be used with a constructor", 110: "Can not use `await` as identifier in module or async func", 111: "Can not use `await` as identifier in module", 112: "HTML comments are only allowed with web compatibility (Annex B)", 113: "The identifier 'let' must not be in expression position in strict mode", 114: "Cannot assign to `eval` and `arguments` in strict mode", 115: "The left-hand side of a for-of loop may not start with 'let'", 116: "Block body arrows can not be immediately invoked without a group", 117: "Block body arrows can not be immediately accessed without a group", 118: "Unexpected strict mode reserved word", 119: "Unexpected eval or arguments in strict mode", 120: "Decorators must not be followed by a semicolon", 121: "Calling delete on expression not allowed in strict mode", 122: "Pattern can not have a tail", 124: "Can not have a `yield` expression on the left side of a ternary", 125: "An arrow function can not have a postfix update operator", 126: "Invalid object literal key character after generator star", 127: "Private fields can not be deleted", 129: "Classes may not have a field called constructor", 128: "Classes may not have a private element named constructor", 130: "A class field initializer or static block may not contain arguments", 131: "Generators can only be declared at the top level or inside a block", 132: "Async methods are a restricted production and cannot have a newline following it", 133: "Unexpected character after object literal property name", 135: "Invalid key token", 136: "Label '%0' has already been declared", 137: "continue statement must be nested within an iteration statement", 138: "Undefined label '%0'", 139: "Trailing comma is disallowed inside import(...) arguments", 140: "Invalid binding in JSON import", 141: "import() requires exactly one argument", 142: "Cannot use new with import(...)", 143: "... is not allowed in import()", 144: "Expected '=>'", 145: "Duplicate binding '%0'", 146: "Duplicate private identifier #%0", 147: "Cannot export a duplicate name '%0'", 150: "Duplicate %0 for-binding", 148: "Exported binding '%0' needs to refer to a top-level declared variable", 149: "Unexpected private field", 153: "Numeric separators are not allowed at the end of numeric literals", 152: "Only one underscore is allowed as numeric separator", 154: "JSX value should be either an expression or a quoted JSX text", 155: "Expected corresponding JSX closing tag for %0", 156: "Adjacent JSX elements must be wrapped in an enclosing tag", 157: "JSX attributes must only be assigned a non-empty 'expression'", 158: "'%0' has already been declared", 159: "'%0' shadowed a catch clause binding", 160: "Dot property must be an identifier", 161: "Encountered invalid input after spread/rest argument", 162: "Catch without try", 163: "Finally without try", 164: "Expected corresponding closing tag for JSX fragment", 165: "Coalescing and logical operators used together in the same expression must be disambiguated with parentheses", 166: "Invalid tagged template on optional chain", 167: "Invalid optional chain from super property", 168: "Invalid optional chain from new expression", 169: "Cannot use \"import.meta\" outside a module", 170: "Leading decorators must be attached to a class declaration", 171: "An export name cannot include a lone surrogate, found %0", 172: "A string literal cannot be used as an exported binding without `from`", 173: "Private fields can't be accessed on super", 174: "The only valid meta property for import is 'import.meta'", 175: "'import.meta' must not contain escaped characters", 176: "cannot use \"await\" as identifier inside an async function", 177: "cannot use \"await\" in static blocks" }; var C$1 = class extends SyntaxError { start; end; range; loc; description; constructor(e, t, n, ...r) { let i = Me[n].replace(/%(\d+)/g, (e, t) => r[t]), a = "[" + e.line + ":" + e.column + "-" + t.line + ":" + t.column + "]: " + i; super(a), this.start = e.index, this.end = t.index, this.range = [e.index, t.index], this.loc = { start: { line: e.line, column: e.column }, end: { line: t.line, column: t.column } }, this.description = i; } }; function Ne(e, t, n) { let r = e.currentChar, i = 0, a = 9, o = n & 64 ? 0 : 1, s = 0, c = 0; if (n & 64) i = "." + Pe(e, r), r = e.currentChar, r === 110 && e.report(12); else { if (r === 48) if (r = _$1(e), (r | 32) == 120) { for (n = 136, r = _$1(e); b$1[r] & 4160;) { if (r === 95) { c || e.report(152), c = 0, r = _$1(e); continue; } c = 1, i = i * 16 + y$1(r), s++, r = _$1(e); } (s === 0 || !c) && e.report(s === 0 ? 21 : 153); } else if ((r | 32) == 111) { for (n = 132, r = _$1(e); b$1[r] & 4128;) { if (r === 95) { c || e.report(152), c = 0, r = _$1(e); continue; } c = 1, i = i * 8 + (r - 48), s++, r = _$1(e); } (s === 0 || !c) && e.report(s === 0 ? 0 : 153); } else if ((r | 32) == 98) { for (n = 130, r = _$1(e); b$1[r] & 4224;) { if (r === 95) { c || e.report(152), c = 0, r = _$1(e); continue; } c = 1, i = i * 2 + (r - 48), s++, r = _$1(e); } (s === 0 || !c) && e.report(s === 0 ? 0 : 153); } else if (b$1[r] & 32) for (t & 1 && e.report(1), n = 1; b$1[r] & 16;) { if (b$1[r] & 512) { n = 32, o = 0; break; } i = i * 8 + (r - 48), r = _$1(e); } else b$1[r] & 512 ? (t & 1 && e.report(1), e.flags |= 64, n = 32) : r === 95 && e.report(0); if (n & 48) { if (o) { for (; a >= 0 && b$1[r] & 4112;) { if (r === 95) { if (r = _$1(e), r === 95 || n & 32) throw new C$1(e.currentLocation, { index: e.index + 1, line: e.line, column: e.column }, 152); c = 1; continue; } c = 0, i = 10 * i + (r - 48), r = _$1(e), --a; } if (c) throw new C$1(e.currentLocation, { index: e.index + 1, line: e.line, column: e.column }, 153); if (a >= 0 && !_e(r) && r !== 46) return e.tokenValue = i, e.options.raw && (e.tokenRaw = e.source.slice(e.tokenIndex, e.index)), 134283266; } i += Pe(e, r), r = e.currentChar, r === 46 && (_$1(e) === 95 && e.report(0), n = 64, i += "." + Pe(e, e.currentChar), r = e.currentChar); } } let l = e.index, u = 0; if (r === 110 && n & 128) u = 1, r = _$1(e); else if ((r | 32) == 101) { r = _$1(e), b$1[r] & 256 && (r = _$1(e)); let { index: t } = e; b$1[r] & 16 || e.report(11), i += e.source.substring(l, t) + Pe(e, r), r = e.currentChar; } return (e.index < e.end && b$1[r] & 16 || _e(r)) && e.report(13), u ? (e.tokenRaw = e.source.slice(e.tokenIndex, e.index), e.tokenValue = BigInt(e.tokenRaw.slice(0, -1).replaceAll("_", "")), 134283388) : (e.tokenValue = n & 15 ? i : n & 32 ? parseFloat(e.source.substring(e.tokenIndex, e.index)) : +i, e.options.raw && (e.tokenRaw = e.source.slice(e.tokenIndex, e.index)), 134283266); } function Pe(e, t) { let n = 0, r = e.index, i = ""; for (; b$1[t] & 4112;) { if (t === 95) { let { index: a } = e; if (t = _$1(e), t === 95) throw new C$1(e.currentLocation, { index: e.index + 1, line: e.line, column: e.column }, 152); n = 1, i += e.source.substring(r, a), r = e.index; continue; } n = 0, t = _$1(e); } if (n) throw new C$1(e.currentLocation, { index: e.index + 1, line: e.line, column: e.column }, 153); return i + e.source.substring(r, e.index); } var w$1 = [ "end of source", "identifier", "number", "string", "regular expression", "false", "true", "null", "template continuation", "template tail", "=>", "(", "{", ".", "...", "}", ")", ";", ",", "[", "]", ":", "?", "'", "\"", "++", "--", "=", "<<=", ">>=", ">>>=", "**=", "+=", "-=", "*=", "/=", "%=", "^=", "|=", "&=", "||=", "&&=", "??=", "typeof", "delete", "void", "!", "~", "+", "-", "in", "instanceof", "*", "%", "/", "**", "&&", "||", "===", "!==", "==", "!=", "<=", ">=", "<", ">", "<<", ">>", ">>>", "&", "|", "^", "var", "let", "const", "break", "case", "catch", "class", "continue", "debugger", "default", "do", "else", "export", "extends", "finally", "for", "function", "if", "import", "new", "return", "super", "switch", "this", "throw", "try", "while", "with", "implements", "interface", "package", "private", "protected", "public", "static", "yield", "as", "async", "await", "constructor", "get", "set", "accessor", "from", "of", "enum", "eval", "arguments", "escaped keyword", "escaped future reserved keyword", "reserved if strict", "#", "BigIntLiteral", "??", "?.", "WhiteSpace", "Illegal", "LineTerminator", "PrivateField", "Template", "@", "target", "meta", "LineFeed", "Escaped", "JSXText" ]; var Fe = { this: 86111, function: 86104, if: 20569, return: 20572, var: 86088, else: 20563, for: 20567, new: 86107, in: 8673330, typeof: 16863275, while: 20578, case: 20556, break: 20555, try: 20577, catch: 20557, delete: 16863276, throw: 86112, switch: 86110, continue: 20559, default: 20561, instanceof: 8411187, do: 20562, void: 16863277, finally: 20566, async: 209005, await: 209006, class: 86094, const: 86090, constructor: 12399, debugger: 20560, export: 20564, extends: 20565, false: 86021, from: 209011, get: 209008, implements: 36964, import: 86106, interface: 36965, let: 241737, null: 86023, of: 471156, package: 36966, private: 36967, protected: 36968, public: 36969, set: 209009, static: 36970, super: 86109, true: 86022, with: 20579, yield: 241771, enum: 86133, eval: 537079926, as: 77932, arguments: 537079927, target: 209029, meta: 209030, accessor: 12402 }; function T$1(e, t) { !(e.flags & 1) && (e.getToken() & 1048576) != 1048576 && e.report(30, w$1[e.getToken() & 255]), E$1(e, t, 1074790417) || e.options.onInsertedSemicolon?.(e.startIndex); } function Ie(e, t, n, r) { return t - n < 13 && r === "use strict" && ((e.getToken() & 1048576) == 1048576 || e.flags & 1) ? 1 : 0; } function Le(e, t, n) { return e.getToken() === n ? (A$1(e, t), 1) : 0; } function E$1(e, t, n) { return e.getToken() === n && (A$1(e, t), !0); } function D$1(e, t, n) { e.getToken() !== n && e.report(25, w$1[n & 255]), A$1(e, t); } function O(e, t) { switch (t.type) { case "ArrayExpression": { t.type = "ArrayPattern"; let { elements: n } = t; for (let t = 0, r = n.length; t < r; ++t) { let r = n[t]; r && O(e, r); } return; } case "ObjectExpression": { t.type = "ObjectPattern"; let { properties: n } = t; for (let t = 0, r = n.length; t < r; ++t) O(e, n[t]); return; } case "AssignmentExpression": t.type = "AssignmentPattern", t.operator !== "=" && e.report(71), delete t.operator, O(e, t.left); return; case "Property": O(e, t.value); return; case "SpreadElement": t.type = "RestElement", O(e, t.argument); } } function Re(e, t, n, r, i) { t & 1 && ((r & 36864) == 36864 && e.report(118), !i && (r & 537079808) == 537079808 && e.report(119)), ((r & 20480) == 20480 || r === -2147483528) && e.report(102), n & 24 && (r & 255) == 73 && e.report(100), t & 2050 && r === 209006 && e.report(110), t & 1025 && r === 241771 && e.report(97, "yield"); } function ze(e, t, n) { t & 1 && ((n & 36864) == 36864 && e.report(118), (n & 537079808) == 537079808 && e.report(119), n === -2147483527 && e.report(95), n === -2147483528 && e.report(95)), (n & 20480) == 20480 && e.report(102), t & 2050 && n === 209006 && e.report(110), t & 1025 && n === 241771 && e.report(97, "yield"); } function Be(e, t, n) { return n === 209006 && (t & 2050 && e.report(110), e.destructible |= 128), n === 241771 && t & 1024 && e.report(97, "yield"), (n & 20480) == 20480 || (n & 36864) == 36864 || n == -2147483527; } function Ve(e) { return e.property ? e.property.type === "PrivateIdentifier" : !1; } function He(e, t, n, r) { for (; t;) { if (t["$" + n]) return r && e.report(137), 1; r && t.loop && (r = 0), t = t.$; } return 0; } function Ue(e, t, n) { let r = t; for (; r;) r["$" + n] && e.report(136, n), r = r.$; t["$" + n] = 1; } function We(e) { switch (e.type) { case "JSXIdentifier": return e.name; case "JSXNamespacedName": return e.namespace + ":" + e.name; case "JSXMemberExpression": return We(e.object) + "." + We(e.property); } } function k$1(e, t) { return e & 1025 ? e & 2 && t === 209006 || e & 1024 && t === 241771 ? !1 : (t & 12288) == 12288 : (t & 12288) == 12288 || (t & 36864) == 36864; } function Ge(e, t, n) { (n & 537079808) == 537079808 && (t & 1 && e.report(119), e.flags |= 512), k$1(t, n) || e.report(0); } function Ke(e, t) { return Object.hasOwn(e, t) ? e[t] : void 0; } function qe(e, t, n) { for (; ge[_$1(e)];); return e.tokenValue = e.source.slice(e.tokenIndex, e.index), e.currentChar !== 92 && e.currentChar <= 126 ? Ke(Fe, e.tokenValue) ?? 208897 : Ye(e, t, 0, n); } function Je(e, t) { let n = Ze(e); return _e(n) || e.report(5), e.tokenValue = String.fromCodePoint(n), Ye(e, t, 1, b$1[n] & 4); } function Ye(e, t, n, r) { let i = e.index; for (; e.index < e.end;) if (e.currentChar === 92) { e.tokenValue += e.source.slice(i, e.index), n = 1; let t = Ze(e); ve(t) || e.report(5), r &&= b$1[t] & 4, e.tokenValue += String.fromCodePoint(t), i = e.index; } else { let t = de(e); if (t > 0) ve(t) || e.report(20, String.fromCodePoint(t)), e.currentChar = t, e.index++, e.column++; else if (!ve(e.currentChar)) break; _$1(e); } e.index <= e.end && (e.tokenValue += e.source.slice(i, e.index)); let { length: a } = e.tokenValue; if (r && a >= 2 && a <= 11) { let r = Ke(Fe, e.tokenValue); return r === void 0 ? 208897 | (n ? -2147483648 : 0) : n ? r === 209006 ? t & 2050 ? -2147483528 : r | -2147483648 : t & 1 ? r === 36970 || (r & 36864) == 36864 ? -2147483527 : (r & 20480) == 20480 ? t & 262144 && !(t & 8) ? r | -2147483648 : -2147483528 : -2147274630 : t & 262144 && !(t & 8) && (r & 20480) == 20480 ? r | -2147483648 : r === 241771 ? t & 262144 ? -2147274630 : t & 1024 ? -2147483528 : r | -2147483648 : r === 209005 ? -2147274630 : (r & 36864) == 36864 ? r | -2147471360 : -2147483528 : r; } return 208897 | (n ? -2147483648 : 0); } function Xe(e) { let t = _$1(e); if (t === 92) return 130; let n = de(e); return n && (t = n), _e(t) || e.report(96), 130; } function Ze(e) { return e.source.charCodeAt(e.index + 1) !== 117 && e.report(5), e.currentChar = e.source.charCodeAt(e.index += 2), e.column += 2, Qe(e); } function Qe(e) { let t = 0, n = e.currentChar; if (n === 123) { let n = e.index - 2; for (; b$1[_$1(e)] & 64;) if (t = t << 4 | y$1(e.currentChar), t > 1114111) throw new C$1({ index: n, line: e.line, column: e.column }, e.currentLocation, 104); if (e.currentChar !== 125) throw new C$1({ index: n, line: e.line, column: e.column }, e.currentLocation, 7); return _$1(e), t; } b$1[n] & 64 || e.report(7); let r = e.source.charCodeAt(e.index + 1); b$1[r] & 64 || e.report(7); let i = e.source.charCodeAt(e.index + 2); b$1[i] & 64 || e.report(7); let a = e.source.charCodeAt(e.index + 3); return b$1[a] & 64 || e.report(7), t = y$1(n) << 12 | y$1(r) << 8 | y$1(i) << 4 | y$1(a), e.currentChar = e.source.charCodeAt(e.index += 4), e.column += 4, t; } var $e = [ 128, 128, 128, 128, 128, 128, 128, 128, 128, 127, 135, 127, 127, 129, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 127, 16842798, 134283267, 130, 208897, 8391477, 8390213, 134283267, 67174411, 16, 8391476, 25233968, 18, 25233969, 67108877, 8457014, 134283266, 134283266, 134283266, 134283266, 134283266, 134283266, 134283266, 134283266, 134283266, 134283266, 21, 1074790417, 8456256, 1077936155, 8390721, 22, 132, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 208897, 69271571, 136, 20, 8389959, 208897, 131, 4096, 4096, 4096, 4096, 4096, 4096, 4096, 208897, 4096, 208897, 208897, 4096, 208897, 4096, 208897, 4096, 208897, 4096, 4096, 4096, 208897, 4096, 4096, 208897, 4096, 4096, 2162700, 8389702, 1074790415, 16842799, 128 ]; function A$1(e, t) { e.flags = (e.flags | 1) ^ 1, e.startIndex = e.index, e.startColumn = e.column, e.startLine = e.line, e.setToken(et(e, t, 0)); } function et(e, t, n) { let r = e.index === 0, { source: i } = e, a = e.currentLocation; for (; e.index < e.end;) { e.tokenIndex = e.index, e.tokenColumn = e.column, e.tokenLine = e.line; let o = e.currentChar; if (o <= 126) { let s = $e[o]; switch (s) { case 67174411: case 16: case 2162700: case 1074790415: case 69271571: case 20: case 21: case 1074790417: case 18: case 16842799: case 132: case 128: return _$1(e), s; case 208897: return qe(e, t, 0); case 4096: return qe(e, t, 1); case 134283266: return Ne(e, t, 144); case 134283267: return Ee(e, t, o); case 131: return ke(e, t); case 136: return Je(e, t); case 130: return Xe(e); case 127: _$1(e); break; case 129: n |= 5, v$1(e); break; case 135: fe(e, n), n = n & -5 | 1; break; case 8456256: { let r = _$1(e); if (e.index < e.end) { if (r === 60) return e.index < e.end && _$1(e) === 61 ? (_$1(e), 4194332) : 8390978; if (r === 61) return _$1(e), 8390718; if (r === 33) { let r = e.index + 1; if (r + 1 < e.end && i.charCodeAt(r) === 45 && i.charCodeAt(r + 1) == 45) { e.column += 3, e.currentChar = i.charCodeAt(e.index += 3), n = xe(e, i, n, t, 2, e.tokenStart), a = e.tokenStart; continue; } return 8456256; } } return 8456256; } case 1077936155: { _$1(e); let t = e.currentChar; return t === 61 ? _$1(e) === 61 ? (_$1(e), 8390458) : 8390460 : t === 62 ? (_$1(e), 10) : 1077936155; } case 16842798: return _$1(e) === 61 ? _$1(e) === 61 ? (_$1(e), 8390459) : 8390461 : 16842798; case 8391477: return _$1(e) === 61 ? (_$1(e), 4194340) : 8391477; case 8391476: { if (_$1(e), e.index >= e.end) return 8391476; let t = e.currentChar; return t === 61 ? (_$1(e), 4194338) : t === 42 ? _$1(e) === 61 ? (_$1(e), 4194335) : 8391735 : 8391476; } case 8389959: return _$1(e) === 61 ? (_$1(e), 4194341) : 8389959; case 25233968: { _$1(e); let t = e.currentChar; return t === 43 ? (_$1(e), 33619993) : t === 61 ? (_$1(e), 4194336) : 25233968; } case 25233969: { _$1(e); let o = e.currentChar; if (o === 45) { if (_$1(e), (n & 1 || r) && e.currentChar === 62) { e.options.webcompat || e.report(112), _$1(e), n = xe(e, i, n, t, 3, a), a = e.tokenStart; continue; } return 33619994; } return o === 61 ? (_$1(e), 4194337) : 25233969; } case 8457014: if (_$1(e), e.index < e.end) { let r = e.currentChar; if (r === 47) { _$1(e), n = Se(e, i, n, 0, e.tokenStart), a = e.tokenStart; continue; } if (r === 42) { _$1(e), n = Ce(e, i, n), a = e.tokenStart; continue; } if (t & 32) return we(e); if (r === 61) return _$1(e), 4259875; } return 8457014; case 67108877: { let n = _$1(e); if (n >= 48 && n <= 57) return Ne(e, t, 80); if (n === 46) { let t = e.index + 1; if (t < e.end && i.charCodeAt(t) === 46) return e.column += 2, e.currentChar = i.charCodeAt(e.index += 2), 14; } return 67108877; } case 8389702: { _$1(e); let t = e.currentChar; return t === 124 ? (_$1(e), e.currentChar === 61 ? (_$1(e), 4194344) : 8913465) : t === 61 ? (_$1(e), 4194342) : 8389702; } case 8390721: { _$1(e); let t = e.currentChar; if (t === 61) return _$1(e), 8390719; if (t !== 62) return 8390721; if (_$1(e), e.index < e.end) { let t = e.currentChar; if (t === 62) return _$1(e) === 61 ? (_$1(e), 4194334) : 8390980; if (t === 61) return _$1(e), 4194333; } return 8390979; } case 8390213: { _$1(e); let t = e.currentChar; return t === 38 ? (_$1(e), e.currentChar === 61 ? (_$1(e), 4194345) : 8913720) : t === 61 ? (_$1(e), 4194343) : 8390213; } case 22: { let t = _$1(e); if (t === 63) return _$1(e), e.currentChar === 61 ? (_$1(e), 4194346) : 276824445; if (t === 46) { let n = e.index + 1; if (n < e.end && (t = i.charCodeAt(n), !(t >= 48 && t <= 57))) return _$1(e), 67108990; } return 22; } } } else { if ((o ^ 8232) <= 1) { n = n & -5 | 1, v$1(e); continue; } let r = de(e); if (r > 0 && (o = r), ue(o)) return e.tokenValue = "", Ye(e, t, 0, 0); if (pe(o)) { _$1(e); continue; } e.report(20, String.fromCodePoint(o)); } } return 1048576; } var tt = { AElig: "Æ", AMP: "&", Aacute: "Á", Abreve: "Ă", Acirc: "Â", Acy: "А", Afr: "𝔄", Agrave: "À", Alpha: "Α", Amacr: "Ā", And: "⩓", Aogon: "Ą", Aopf: "𝔸", ApplyFunction: "⁡", Aring: "Å", Ascr: "𝒜", Assign: "≔", Atilde: "Ã", Auml: "Ä", Backslash: "∖", Barv: "⫧", Barwed: "⌆", Bcy: "Б", Because: "∵", Bernoullis: "ℬ", Beta: "Β", Bfr: "𝔅", Bopf: "𝔹", Breve: "˘", Bscr: "ℬ", Bumpeq: "≎", CHcy: "Ч", COPY: "©", Cacute: "Ć", Cap: "⋒", CapitalDifferentialD: "ⅅ", Cayleys: "ℭ", Ccaron: "Č", Ccedil: "Ç", Ccirc: "Ĉ", Cconint: "∰", Cdot: "Ċ", Cedilla: "¸", CenterDot: "·", Cfr: "ℭ", Chi: "Χ", CircleDot: "⊙", CircleMinus: "⊖", CirclePlus: "⊕", CircleTimes: "⊗", ClockwiseContourIntegral: "∲", CloseCurlyDoubleQuote: "”", CloseCurlyQuote: "’", Colon: "∷", Colone: "⩴", Congruent: "≡", Conint: "∯", ContourIntegral: "∮", Copf: "ℂ", Coproduct: "∐", CounterClockwiseContourIntegral: "∳", Cross: "⨯", Cscr: "𝒞", Cup: "⋓", CupCap: "≍", DD: "ⅅ", DDotrahd: "⤑", DJcy: "Ђ", DScy: "Ѕ", DZcy: "Џ", Dagger: "‡", Darr: "↡", Dashv: "⫤", Dcaron: "Ď", Dcy: "Д", Del: "∇", Delta: "Δ", Dfr: "𝔇", DiacriticalAcute: "´", DiacriticalDot: "˙", DiacriticalDoubleAcute: "˝", DiacriticalGrave: "`", DiacriticalTilde: "˜", Diamond: "⋄", DifferentialD: "ⅆ", Dopf: "𝔻", Dot: "¨", DotDot: "⃜", DotEqual: "≐", DoubleContourIntegral: "∯", DoubleDot: "¨", DoubleDownArrow: "⇓", DoubleLeftArrow: "⇐", DoubleLeftRightArrow: "⇔", DoubleLeftTee: "⫤", DoubleLongLeftArrow: "⟸", DoubleLongLeftRightArrow: "⟺", DoubleLongRightArrow: "⟹", DoubleRightArrow: "⇒", DoubleRightTee: "⊨", DoubleUpArrow: "⇑", DoubleUpDownArrow: "⇕", DoubleVerticalBar: "∥", DownArrow: "↓", DownArrowBar: "⤓", DownArrowUpArrow: "⇵", DownBreve: "̑", DownLeftRightVector: "⥐", DownLeftTeeVector: "⥞", DownLeftVector: "↽", DownLeftVectorBar: "⥖", DownRightTeeVector: "⥟", DownRightVector: "⇁", DownRightVectorBar: "⥗", DownTee: "⊤", DownTeeArrow: "↧", Downarrow: "⇓", Dscr: "𝒟", Dstrok: "Đ", ENG: "Ŋ", ETH: "Ð", Eacute: "É", Ecaron: "Ě", Ecirc: "Ê", Ecy: "Э", Edot: "Ė", Efr: "𝔈", Egrave: "È", Element: "∈", Emacr: "Ē", EmptySmallSquare: "◻", EmptyVerySmallSquare: "▫", Eogon: "Ę", Eopf: "𝔼", Epsilon: "Ε", Equal: "⩵", EqualTilde: "≂", Equilibrium: "⇌", Escr: "ℰ", Esim: "⩳", Eta: "Η", Euml: "Ë", Exists: "∃", ExponentialE: "ⅇ", Fcy: "Ф", Ffr: "𝔉", FilledSmallSquare: "◼", FilledVerySmallSquare: "▪", Fopf: "𝔽", ForAll: "∀", Fouriertrf: "ℱ", Fscr: "ℱ", GJcy: "Ѓ", GT: ">", Gamma: "Γ", Gammad: "Ϝ", Gbreve: "Ğ", Gcedil: "Ģ", Gcirc: "Ĝ", Gcy: "Г", Gdot: "Ġ", Gfr: "𝔊", Gg: "⋙", Gopf: "𝔾", GreaterEqual: "≥", GreaterEqualLess: "⋛", GreaterFullEqual: "≧", GreaterGreater: "⪢", GreaterLess: "≷", GreaterSlantEqual: "⩾", GreaterTilde: "≳", Gscr: "𝒢", Gt: "≫", HARDcy: "Ъ", Hacek: "ˇ", Hat: "^", Hcirc: "Ĥ", Hfr: "ℌ", HilbertSpace: "ℋ", Hopf: "ℍ", HorizontalLine: "─", Hscr: "ℋ", Hstrok: "Ħ", HumpDownHump: "≎", HumpEqual: "≏", IEcy: "Е", IJlig: "IJ", IOcy: "Ё", Iacute: "Í", Icirc: "Î", Icy: "И", Idot: "İ", Ifr: "ℑ", Igrave: "Ì", Im: "ℑ", Imacr: "Ī", ImaginaryI: "ⅈ", Implies: "⇒", Int: "∬", Integral: "∫", Intersection: "⋂", InvisibleComma: "⁣", InvisibleTimes: "⁢", Iogon: "Į", Iopf: "𝕀", Iota: "Ι", Iscr: "ℐ", Itilde: "Ĩ", Iukcy: "І", Iuml: "Ï", Jcirc: "Ĵ", Jcy: "Й", Jfr: "𝔍", Jopf: "𝕁", Jscr: "𝒥", Jsercy: "Ј", Jukcy: "Є", KHcy: "Х", KJcy: "Ќ", Kappa: "Κ", Kcedil: "Ķ", Kcy: "К", Kfr: "𝔎", Kopf: "𝕂", Kscr: "𝒦", LJcy: "Љ", LT: "<", Lacute: "Ĺ", Lambda: "Λ", Lang: "⟪", Laplacetrf: "ℒ", Larr: "↞", Lcaron: "Ľ", Lcedil: "Ļ", Lcy: "Л", LeftAngleBracket: "⟨", LeftArrow: "←", LeftArrowBar: "⇤", LeftArrowRightArrow: "⇆", LeftCeiling: "⌈", LeftDoubleBracket: "⟦", LeftDownTeeVector: "⥡", LeftDownVector: "⇃", LeftDownVectorBar: "⥙", LeftFloor: "⌊", LeftRightArrow: "↔", LeftRightVector: "⥎", LeftTee: "⊣", LeftTeeArrow: "↤", LeftTeeVector: "⥚", LeftTriangle: "⊲", LeftTriangleBar: "⧏", LeftTriangleEqual: "⊴", LeftUpDownVector: "⥑", LeftUpTeeVector: "⥠", LeftUpVector: "↿", LeftUpVectorBar: "⥘", LeftVector: "↼", LeftVectorBar: "⥒", Leftarrow: "⇐", Leftrightarrow: "⇔", LessEqualGreater: "⋚", LessFullEqual: "≦", LessGreater: "≶", LessLess: "⪡", LessSlantEqual: "⩽", LessTilde: "≲", Lfr: "𝔏", Ll: "⋘", Lleftarrow: "⇚", Lmidot: "Ŀ", LongLeftArrow: "⟵", LongLeftRightArrow: "⟷", LongRightArrow: "⟶", Longleftarrow: "⟸", Longleftrightarrow: "⟺", Longrightarrow: "⟹", Lopf: "𝕃", LowerLeftArrow: "↙", LowerRightArrow: "↘", Lscr: "ℒ", Lsh: "↰", Lstrok: "Ł", Lt: "≪", Map: "⤅", Mcy: "М", MediumSpace: " ", Mellintrf: "ℳ", Mfr: "𝔐", MinusPlus: "∓", Mopf: "𝕄", Mscr: "ℳ", Mu: "Μ", NJcy: "Њ", Nacute: "Ń", Ncaron: "Ň", Ncedil: "Ņ", Ncy: "Н", NegativeMediumSpace: "​", NegativeThickSpace: "​", NegativeThinSpace: "​", NegativeVeryThinSpace: "​", NestedGreaterGreater: "≫", NestedLessLess: "≪", NewLine: "\n", Nfr: "𝔑", NoBreak: "⁠", NonBreakingSpace: "\xA0", Nopf: "ℕ", Not: "⫬", NotCongruent: "≢", NotCupCap: "≭", NotDoubleVerticalBar: "∦", NotElement: "∉", NotEqual: "≠", NotEqualTilde: "≂̸", NotExists: "∄", NotGreater: "≯", NotGreaterEqual: "≱", NotGreaterFullEqual: "≧̸", NotGreaterGreater: "≫̸", NotGreaterLess: "≹", NotGreaterSlantEqual: "⩾̸", NotGreaterTilde: "≵", NotHumpDownHump: "≎̸", NotHumpEqual: "≏̸", NotLeftTriangle: "⋪", NotLeftTriangleBar: "⧏̸", NotLeftTriangleEqual: "⋬", NotLess: "≮", NotLessEqual: "≰", NotLessGreater: "≸", NotLessLess: "≪̸", NotLessSlantEqual: "⩽̸", NotLessTilde: "≴", NotNestedGreaterGreater: "⪢̸", NotNestedLessLess: "⪡̸", NotPrecedes: "⊀", NotPrecedesEqual: "⪯̸", NotPrecedesSlantEqual: "⋠", NotReverseElement: "∌", NotRightTriangle: "⋫", NotRightTriangleBar: "⧐̸", NotRightTriangleEqual: "⋭", NotSquareSubset: "⊏̸", NotSquareSubsetEqual: "⋢", NotSquareSuperset: "⊐̸", NotSquareSupersetEqual: "⋣", NotSubset: "⊂⃒", NotSubsetEqual: "⊈", NotSucceeds: "⊁", NotSucceedsEqual: "⪰̸", NotSucceedsSlantEqual: "⋡", NotSucceedsTilde: "≿̸", NotSuperset: "⊃⃒", NotSupersetEqual: "⊉", NotTilde: "≁", NotTildeEqual: "≄", NotTildeFullEqual: "≇", NotTildeTilde: "≉", NotVerticalBar: "∤", Nscr: "𝒩", Ntilde: "Ñ", Nu: "Ν", OElig: "Œ", Oacute: "Ó", Ocirc: "Ô", Ocy: "О", Odblac: "Ő", Ofr: "𝔒", Ograve: "Ò", Omacr: "Ō", Omega: "Ω", Omicron: "Ο", Oopf: "𝕆", OpenCurlyDoubleQuote: "“", OpenCurlyQuote: "‘", Or: "⩔", Oscr: "𝒪", Oslash: "Ø", Otilde: "Õ", Otimes: "⨷", Ouml: "Ö", OverBar: "‾", OverBrace: "⏞", OverBracket: "⎴", OverParenthesis: "⏜", PartialD: "∂", Pcy: "П", Pfr: "𝔓", Phi: "Φ", Pi: "Π", PlusMinus: "±", Poincareplane: "ℌ", Popf: "ℙ", Pr: "⪻", Precedes: "≺", PrecedesEqual: "⪯", PrecedesSlantEqual: "≼", PrecedesTilde: "≾", Prime: "″", Product: "∏", Proportion: "∷", Proportional: "∝", Pscr: "𝒫", Psi: "Ψ", QUOT: "\"", Qfr: "𝔔", Qopf: "ℚ", Qscr: "𝒬", RBarr: "⤐", REG: "®", Racute: "Ŕ", Rang: "⟫", Rarr: "↠", Rarrtl: "⤖", Rcaron: "Ř", Rcedil: "Ŗ", Rcy: "Р", Re: "ℜ", ReverseElement: "∋", ReverseEquilibrium: "⇋", ReverseUpEquilibrium: "⥯", Rfr: "ℜ", Rho: "Ρ", RightAngleBracket: "⟩", RightArrow: "→", RightArrowBar: "⇥", RightArrowLeftArrow: "⇄", RightCeiling: "⌉", RightDoubleBracket: "⟧", RightDownTeeVector: "⥝", RightDownVector: "⇂", RightDownVectorBar: "⥕", RightFloor: "⌋", RightTee: "⊢", RightTeeArrow: "↦", RightTeeVector: "⥛", RightTriangle: "⊳", RightTriangleBar: "⧐", RightTriangleEqual: "⊵", RightUpDownVector: "⥏", RightUpTeeVector: "⥜", RightUpVector: "↾", RightUpVectorBar: "⥔", RightVector: "⇀", RightVectorBar: "⥓", Rightarrow: "⇒", Ropf: "ℝ", RoundImplies: "⥰", Rrightarrow: "⇛", Rscr: "ℛ", Rsh: "↱", RuleDelayed: "⧴", SHCHcy: "Щ", SHcy: "Ш", SOFTcy: "Ь", Sacute: "Ś", Sc: "⪼", Scaron: "Š", Scedil: "Ş", Scirc: "Ŝ", Scy: "С", Sfr: "𝔖", ShortDownArrow: "↓", ShortLeftArrow: "←", ShortRightArrow: "→", ShortUpArrow: "↑", Sigma: "Σ", SmallCircle: "∘", Sopf: "𝕊", Sqrt: "√", Square: "□", SquareIntersection: "⊓", SquareSubset: "⊏", SquareSubsetEqual: "⊑", SquareSuperset: "⊐", SquareSupersetEqual: "⊒", SquareUnion: "⊔", Sscr: "𝒮", Star: "⋆", Sub: "⋐", Subset: "⋐", SubsetEqual: "⊆", Succeeds: "≻", SucceedsEqual: "⪰", SucceedsSlantEqual: "≽", SucceedsTilde: "≿", SuchThat: "∋", Sum: "∑", Sup: "⋑", Superset: "⊃", SupersetEqual: "⊇", Supset: "⋑", THORN: "Þ", TRADE: "™", TSHcy: "Ћ", TScy: "Ц", Tab: " ", Tau: "Τ", Tcaron: "Ť", Tcedil: "Ţ", Tcy: "Т", Tfr: "𝔗", Therefore: "∴", Theta: "Θ", ThickSpace: "  ", ThinSpace: " ", Tilde: "∼", TildeEqual: "≃", TildeFullEqual: "≅", TildeTilde: "≈", Topf: "𝕋", TripleDot: "⃛", Tscr: "𝒯", Tstrok: "Ŧ", Uacute: "Ú", Uarr: "↟", Uarrocir: "⥉", Ubrcy: "Ў", Ubreve: "Ŭ", Ucirc: "Û", Ucy: "У", Udblac: "Ű", Ufr: "𝔘", Ugrave: "Ù", Umacr: "Ū", UnderBar: "_", UnderBrace: "⏟", UnderBracket: "⎵", UnderParenthesis: "⏝", Union: "⋃", UnionPlus: "⊎", Uogon: "Ų", Uopf: "𝕌", UpArrow: "↑", UpArrowBar: "⤒", UpArrowDownArrow: "⇅", UpDownArrow: "↕", UpEquilibrium: "⥮", UpTee: "⊥", UpTeeArrow: "↥", Uparrow: "⇑", Updownarrow: "⇕", UpperLeftArrow: "↖", UpperRightArrow: "↗", Upsi: "ϒ", Upsilon: "Υ", Uring: "Ů", Uscr: "𝒰", Utilde: "Ũ", Uuml: "Ü", VDash: "⊫", Vbar: "⫫", Vcy: "В", Vdash: "⊩", Vdashl: "⫦", Vee: "⋁", Verbar: "‖", Vert: "‖", VerticalBar: "∣", VerticalLine: "|", VerticalSeparator: "❘", VerticalTilde: "≀", VeryThinSpace: " ", Vfr: "𝔙", Vopf: "𝕍", Vscr: "𝒱", Vvdash: "⊪", Wcirc: "Ŵ", Wedge: "⋀", Wfr: "𝔚", Wopf: "𝕎", Wscr: "𝒲", Xfr: "𝔛", Xi: "Ξ", Xopf: "𝕏", Xscr: "𝒳", YAcy: "Я", YIcy: "Ї", YUcy: "Ю", Yacute: "Ý", Ycirc: "Ŷ", Ycy: "Ы", Yfr: "𝔜", Yopf: "𝕐", Yscr: "𝒴", Yuml: "Ÿ", ZHcy: "Ж", Zacute: "Ź", Zcaron: "Ž", Zcy: "З", Zdot: "Ż", ZeroWidthSpace: "​", Zeta: "Ζ", Zfr: "ℨ", Zopf: "ℤ", Zscr: "𝒵", aacute: "á", abreve: "ă", ac: "∾", acE: "∾̳", acd: "∿", acirc: "â", acute: "´", acy: "а", aelig: "æ", af: "⁡", afr: "𝔞", agrave: "à", alefsym: "ℵ", aleph: "ℵ", alpha: "α", amacr: "ā", amalg: "⨿", amp: "&", and: "∧", andand: "⩕", andd: "⩜", andslope: "⩘", andv: "⩚", ang: "∠", ange: "⦤", angle: "∠", angmsd: "∡", angmsdaa: "⦨", angmsdab: "⦩", angmsdac: "⦪", angmsdad: "⦫", angmsdae: "⦬", angmsdaf: "⦭", angmsdag: "⦮", angmsdah: "⦯", angrt: "∟", angrtvb: "⊾", angrtvbd: "⦝", angsph: "∢", angst: "Å", angzarr: "⍼", aogon: "ą", aopf: "𝕒", ap: "≈", apE: "⩰", apacir: "⩯", ape: "≊", apid: "≋", apos: "'", approx: "≈", approxeq: "≊", aring: "å", ascr: "𝒶", ast: "*", asymp: "≈", asympeq: "≍", atilde: "ã", auml: "ä", awconint: "∳", awint: "⨑", bNot: "⫭", backcong: "≌", backepsilon: "϶", backprime: "‵", backsim: "∽", backsimeq: "⋍", barvee: "⊽", barwed: "⌅", barwedge: "⌅", bbrk: "⎵", bbrktbrk: "⎶", bcong: "≌", bcy: "б", bdquo: "„", becaus: "∵", because: "∵", bemptyv: "⦰", bepsi: "϶", bernou: "ℬ", beta: "β", beth: "ℶ", between: "≬", bfr: "𝔟", bigcap: "⋂", bigcirc: "◯", bigcup: "⋃", bigodot: "⨀", bigoplus: "⨁", bigotimes: "⨂", bigsqcup: "⨆", bigstar: "★", bigtriangledown: "▽", bigtriangleup: "△", biguplus: "⨄", bigvee: "⋁", bigwedge: "⋀", bkarow: "⤍", blacklozenge: "⧫", blacksquare: "▪", blacktriangle: "▴", blacktriangledown: "▾", blacktriangleleft: "◂", blacktriangleright: "▸", blank: "␣", blk12: "▒", blk14: "░", blk34: "▓", block: "█", bne: "=⃥", bnequiv: "≡⃥", bnot: "⌐", bopf: "𝕓", bot: "⊥", bottom: "⊥", bowtie: "⋈", boxDL: "╗", boxDR: "╔", boxDl: "╖", boxDr: "╓", boxH: "═", boxHD: "╦", boxHU: "╩", boxHd: "╤", boxHu: "╧", boxUL: "╝", boxUR: "╚", boxUl: "╜", boxUr: "╙", boxV: "║", boxVH: "╬", boxVL: "╣", boxVR: "╠", boxVh: "╫", boxVl: "╢", boxVr: "╟", boxbox: "⧉", boxdL: "╕", boxdR: "╒", boxdl: "┐", boxdr: "┌", boxh: "─", boxhD: "╥", boxhU: "╨", boxhd: "┬", boxhu: "┴", boxminus: "⊟", boxplus: "⊞", boxtimes: "⊠", boxuL: "╛", boxuR: "╘", boxul: "┘", boxur: "└", boxv: "│", boxvH: "╪", boxvL: "╡", boxvR: "╞", boxvh: "┼", boxvl: "┤", boxvr: "├", bprime: "‵", breve: "˘", brvbar: "¦", bscr: "𝒷", bsemi: "⁏", bsim: "∽", bsime: "⋍", bsol: "\\", bsolb: "⧅", bsolhsub: "⟈", bull: "•", bullet: "•", bump: "≎", bumpE: "⪮", bumpe: "≏", bumpeq: "≏", cacute: "ć", cap: "∩", capand: "⩄", capbrcup: "⩉", capcap: "⩋", capcup: "⩇", capdot: "⩀", caps: "∩︀", caret: "⁁", caron: "ˇ", ccaps: "⩍", ccaron: "č", ccedil: "ç", ccirc: "ĉ", ccups: "⩌", ccupssm: "⩐", cdot: "ċ", cedil: "¸", cemptyv: "⦲", cent: "¢", centerdot: "·", cfr: "𝔠", chcy: "ч", check: "✓", checkmark: "✓", chi: "χ", cir: "○", cirE: "⧃", circ: "ˆ", circeq: "≗", circlearrowleft: "↺", circlearrowright: "↻", circledR: "®", circledS: "Ⓢ", circledast: "⊛", circledcirc: "⊚", circleddash: "⊝", cire: "≗", cirfnint: "⨐", cirmid: "⫯", cirscir: "⧂", clubs: "♣", clubsuit: "♣", colon: ":", colone: "≔", coloneq: "≔", comma: ",", commat: "@", comp: "∁", compfn: "∘", complement: "∁", complexes: "ℂ", cong: "≅", congdot: "⩭", conint: "∮", copf: "𝕔", coprod: "∐", copy: "©", copysr: "℗", crarr: "↵", cross: "✗", cscr: "𝒸", csub: "⫏", csube: "⫑", csup: "⫐", csupe: "⫒", ctdot: "⋯", cudarrl: "⤸", cudarrr: "⤵", cuepr: "⋞", cuesc: "⋟", cularr: "↶", cularrp: "⤽", cup: "∪", cupbrcap: "⩈", cupcap: "⩆", cupcup: "⩊", cupdot: "⊍", cupor: "⩅", cups: "∪︀", curarr: "↷", curarrm: "⤼", curlyeqprec: "⋞", curlyeqsucc: "⋟", curlyvee: "⋎", curlywedge: "⋏", curren: "¤", curvearrowleft: "↶", curvearrowright: "↷", cuvee: "⋎", cuwed: "⋏", cwconint: "∲", cwint: "∱", cylcty: "⌭", dArr: "⇓", dHar: "⥥", dagger: "†", daleth: "ℸ", darr: "↓", dash: "‐", dashv: "⊣", dbkarow: "⤏", dblac: "˝", dcaron: "ď", dcy: "д", dd: "ⅆ", ddagger: "‡", ddarr: "⇊", ddotseq: "⩷", deg: "°", delta: "δ", demptyv: "⦱", dfisht: "⥿", dfr: "𝔡", dharl: "⇃", dharr: "⇂", diam: "⋄", diamond: "⋄", diamondsuit: "♦", diams: "♦", die: "¨", digamma: "ϝ", disin: "⋲", div: "÷", divide: "÷", divideontimes: "⋇", divonx: "⋇", djcy: "ђ", dlcorn: "⌞", dlcrop: "⌍", dollar: "$", dopf: "𝕕", dot: "˙", doteq: "≐", doteqdot: "≑", dotminus: "∸", dotplus: "∔", dotsquare: "⊡", doublebarwedge: "⌆", downarrow: "↓", downdownarrows: "⇊", downharpoonleft: "⇃", downharpoonright: "⇂", drbkarow: "⤐", drcorn: "⌟", drcrop: "⌌", dscr: "𝒹", dscy: "ѕ", dsol: "⧶", dstrok: "đ", dtdot: "⋱", dtri: "▿", dtrif: "▾", duarr: "⇵", duhar: "⥯", dwangle: "⦦", dzcy: "џ", dzigrarr: "⟿", eDDot: "⩷", eDot: "≑", eacute: "é", easter: "⩮", ecaron: "ě", ecir: "≖", ecirc: "ê", ecolon: "≕", ecy: "э", edot: "ė", ee: "ⅇ", efDot: "≒", efr: "𝔢", eg: "⪚", egrave: "è", egs: "⪖", egsdot: "⪘", el: "⪙", elinters: "⏧", ell: "ℓ", els: "⪕", elsdot: "⪗", emacr: "ē", empty: "∅", emptyset: "∅", emptyv: "∅", emsp13: " ", emsp14: " ", emsp: " ", eng: "ŋ", ensp: " ", eogon: "ę", eopf: "𝕖", epar: "⋕", eparsl: "⧣", eplus: "⩱", epsi: "ε", epsilon: "ε", epsiv: "ϵ", eqcirc: "≖", eqcolon: "≕", eqsim: "≂", eqslantgtr: "⪖", eqslantless: "⪕", equals: "=", equest: "≟", equiv: "≡", equivDD: "⩸", eqvparsl: "⧥", erDot: "≓", erarr: "⥱", escr: "ℯ", esdot: "≐", esim: "≂", eta: "η", eth: "ð", euml: "ë", euro: "€", excl: "!", exist: "∃", expectation: "ℰ", exponentiale: "ⅇ", fallingdotseq: "≒", fcy: "ф", female: "♀", ffilig: "ffi", fflig: "ff", ffllig: "ffl", ffr: "𝔣", filig: "fi", fjlig: "fj", flat: "♭", fllig: "fl", fltns: "▱", fnof: "ƒ", fopf: "𝕗", forall: "∀", fork: "⋔", forkv: "⫙", fpartint: "⨍", frac12: "½", frac13: "⅓", frac14: "¼", frac15: "⅕", frac16: "⅙", frac18: "⅛", frac23: "⅔", frac25: "⅖", frac34: "¾", frac35: "⅗", frac38: "⅜", frac45: "⅘", frac56: "⅚", frac58: "⅝", frac78: "⅞", frasl: "⁄", frown: "⌢", fscr: "𝒻", gE: "≧", gEl: "⪌", gacute: "ǵ", gamma: "γ", gammad: "ϝ", gap: "⪆", gbreve: "ğ", gcirc: "ĝ", gcy: "г", gdot: "ġ", ge: "≥", gel: "⋛", geq: "≥", geqq: "≧", geqslant: "⩾", ges: "⩾", gescc: "⪩", gesdot: "⪀", gesdoto: "⪂", gesdotol: "⪄", gesl: "⋛︀", gesles: "⪔", gfr: "𝔤", gg: "≫", ggg: "⋙", gimel: "ℷ", gjcy: "ѓ", gl: "≷", glE: "⪒", gla: "⪥", glj: "⪤", gnE: "≩", gnap: "⪊", gnapprox: "⪊", gne: "⪈", gneq: "⪈", gneqq: "≩", gnsim: "⋧", gopf: "𝕘", grave: "`", gscr: "ℊ", gsim: "≳", gsime: "⪎", gsiml: "⪐", gt: ">", gtcc: "⪧", gtcir: "⩺", gtdot: "⋗", gtlPar: "⦕", gtquest: "⩼", gtrapprox: "⪆", gtrarr: "⥸", gtrdot: "⋗", gtreqless: "⋛", gtreqqless: "⪌", gtrless: "≷", gtrsim: "≳", gvertneqq: "≩︀", gvnE: "≩︀", hArr: "⇔", hairsp: " ", half: "½", hamilt: "ℋ", hardcy: "ъ", harr: "↔", harrcir: "⥈", harrw: "↭", hbar: "ℏ", hcirc: "ĥ", hearts: "♥", heartsuit: "♥", hellip: "…", hercon: "⊹", hfr: "𝔥", hksearow: "⤥", hkswarow: "⤦", hoarr: "⇿", homtht: "∻", hookleftarrow: "↩", hookrightarrow: "↪", hopf: "𝕙", horbar: "―", hscr: "𝒽", hslash: "ℏ", hstrok: "ħ", hybull: "⁃", hyphen: "‐", iacute: "í", ic: "⁣", icirc: "î", icy: "и", iecy: "е", iexcl: "¡", iff: "⇔", ifr: "𝔦", igrave: "ì", ii: "ⅈ", iiiint: "⨌", iiint: "∭", iinfin: "⧜", iiota: "℩", ijlig: "ij", imacr: "ī", image: "ℑ", imagline: "ℐ", imagpart: "ℑ", imath: "ı", imof: "⊷", imped: "Ƶ", in: "∈", incare: "℅", infin: "∞", infintie: "⧝", inodot: "ı", int: "∫", intcal: "⊺", integers: "ℤ", intercal: "⊺", intlarhk: "⨗", intprod: "⨼", iocy: "ё", iogon: "į", iopf: "𝕚", iota: "ι", iprod: "⨼", iquest: "¿", iscr: "𝒾", isin: "∈", isinE: "⋹", isindot: "⋵", isins: "⋴", isinsv: "⋳", isinv: "∈", it: "⁢", itilde: "ĩ", iukcy: "і", iuml: "ï", jcirc: "ĵ", jcy: "й", jfr: "𝔧", jmath: "ȷ", jopf: "𝕛", jscr: "𝒿", jsercy: "ј", jukcy: "є", kappa: "κ", kappav: "ϰ", kcedil: "ķ", kcy: "к", kfr: "𝔨", kgreen: "ĸ", khcy: "х", kjcy: "ќ", kopf: "𝕜", kscr: "𝓀", lAarr: "⇚", lArr: "⇐", lAtail: "⤛", lBarr: "⤎", lE: "≦", lEg: "⪋", lHar: "⥢", lacute: "ĺ", laemptyv: "⦴", lagran: "ℒ", lambda: "λ", lang: "⟨", langd: "⦑", langle: "⟨", lap: "⪅", laquo: "«", larr: "←", larrb: "⇤", larrbfs: "⤟", larrfs: "⤝", larrhk: "↩", larrlp: "↫", larrpl: "⤹", larrsim: "⥳", larrtl: "↢", lat: "⪫", latail: "⤙", late: "⪭", lates: "⪭︀", lbarr: "⤌", lbbrk: "❲", lbrace: "{", lbrack: "[", lbrke: "⦋", lbrksld: "⦏", lbrkslu: "⦍", lcaron: "ľ", lcedil: "ļ", lceil: "⌈", lcub: "{", lcy: "л", ldca: "⤶", ldquo: "“", ldquor: "„", ldrdhar: "⥧", ldrushar: "⥋", ldsh: "↲", le: "≤", leftarrow: "←", leftarrowtail: "↢", leftharpoondown: "↽", leftharpoonup: "↼", leftleftarrows: "⇇", leftrightarrow: "↔", leftrightarrows: "⇆", leftrightharpoons: "⇋", leftrightsquigarrow: "↭", leftthreetimes: "⋋", leg: "⋚", leq: "≤", leqq: "≦", leqslant: "⩽", les: "⩽", lescc: "⪨", lesdot: "⩿", lesdoto: "⪁", lesdotor: "⪃", lesg: "⋚︀", lesges: "⪓", lessapprox: "⪅", lessdot: "⋖", lesseqgtr: "⋚", lesseqqgtr: "⪋", lessgtr: "≶", lesssim: "≲", lfisht: "⥼", lfloor: "⌊", lfr: "𝔩", lg: "≶", lgE: "⪑", lhard: "↽", lharu: "↼", lharul: "⥪", lhblk: "▄", ljcy: "љ", ll: "≪", llarr: "⇇", llcorner: "⌞", llhard: "⥫", lltri: "◺", lmidot: "ŀ", lmoust: "⎰", lmoustache: "⎰", lnE: "≨", lnap: "⪉", lnapprox: "⪉", lne: "⪇", lneq: "⪇", lneqq: "≨", lnsim: "⋦", loang: "⟬", loarr: "⇽", lobrk: "⟦", longleftarrow: "⟵", longleftrightarrow: "⟷", longmapsto: "⟼", longrightarrow: "⟶", looparrowleft: "↫", looparrowright: "↬", lopar: "⦅", lopf: "𝕝", loplus: "⨭", lotimes: "⨴", lowast: "∗", lowbar: "_", loz: "◊", lozenge: "◊", lozf: "⧫", lpar: "(", lparlt: "⦓", lrarr: "⇆", lrcorner: "⌟", lrhar: "⇋", lrhard: "⥭", lrm: "‎", lrtri: "⊿", lsaquo: "‹", lscr: "𝓁", lsh: "↰", lsim: "≲", lsime: "⪍", lsimg: "⪏", lsqb: "[", lsquo: "‘", lsquor: "‚", lstrok: "ł", lt: "<", ltcc: "⪦", ltcir: "⩹", ltdot: "⋖", lthree: "⋋", ltimes: "⋉", ltlarr: "⥶", ltquest: "⩻", ltrPar: "⦖", ltri: "◃", ltrie: "⊴", ltrif: "◂", lurdshar: "⥊", luruhar: "⥦", lvertneqq: "≨︀", lvnE: "≨︀", mDDot: "∺", macr: "¯", male: "♂", malt: "✠", maltese: "✠", map: "↦", mapsto: "↦", mapstodown: "↧", mapstoleft: "↤", mapstoup: "↥", marker: "▮", mcomma: "⨩", mcy: "м", mdash: "—", measuredangle: "∡", mfr: "𝔪", mho: "℧", micro: "µ", mid: "∣", midast: "*", midcir: "⫰", middot: "·", minus: "−", minusb: "⊟", minusd: "∸", minusdu: "⨪", mlcp: "⫛", mldr: "…", mnplus: "∓", models: "⊧", mopf: "𝕞", mp: "∓", mscr: "𝓂", mstpos: "∾", mu: "μ", multimap: "⊸", mumap: "⊸", nGg: "⋙̸", nGt: "≫⃒", nGtv: "≫̸", nLeftarrow: "⇍", nLeftrightarrow: "⇎", nLl: "⋘̸", nLt: "≪⃒", nLtv: "≪̸", nRightarrow: "⇏", nVDash: "⊯", nVdash: "⊮", nabla: "∇", nacute: "ń", nang: "∠⃒", nap: "≉", napE: "⩰̸", napid: "≋̸", napos: "ʼn", napprox: "≉", natur: "♮", natural: "♮", naturals: "ℕ", nbsp: "\xA0", nbump: "≎̸", nbumpe: "≏̸", ncap: "⩃", ncaron: "ň", ncedil: "ņ", ncong: "≇", ncongdot: "⩭̸", ncup: "⩂", ncy: "н", ndash: "–", ne: "≠", neArr: "⇗", nearhk: "⤤", nearr: "↗", nearrow: "↗", nedot: "≐̸", nequiv: "≢", nesear: "⤨", nesim: "≂̸", nexist: "∄", nexists: "∄", nfr: "𝔫", ngE: "≧̸", nge: "≱", ngeq: "≱", ngeqq: "≧̸", ngeqslant: "⩾̸", nges: "⩾̸", ngsim: "≵", ngt: "≯", ngtr: "≯", nhArr: "⇎", nharr: "↮", nhpar: "⫲", ni: "∋", nis: "⋼", nisd: "⋺", niv: "∋", njcy: "њ", nlArr: "⇍", nlE: "≦̸", nlarr: "↚", nldr: "‥", nle: "≰", nleftarrow: "↚", nleftrightarrow: "↮", nleq: "≰", nleqq: "≦̸", nleqslant: "⩽̸", nles: "⩽̸", nless: "≮", nlsim: "≴", nlt: "≮", nltri: "⋪", nltrie: "⋬", nmid: "∤", nopf: "𝕟", not: "¬", notin: "∉", notinE: "⋹̸", notindot: "⋵̸", notinva: "∉", notinvb: "⋷", notinvc: "⋶", notni: "∌", notniva: "∌", notnivb: "⋾", notnivc: "⋽", npar: "∦", nparallel: "∦", nparsl: "⫽⃥", npart: "∂̸", npolint: "⨔", npr: "⊀", nprcue: "⋠", npre: "⪯̸", nprec: "⊀", npreceq: "⪯̸", nrArr: "⇏", nrarr: "↛", nrarrc: "⤳̸", nrarrw: "↝̸", nrightarrow: "↛", nrtri: "⋫", nrtrie: "⋭", nsc: "⊁", nsccue: "⋡", nsce: "⪰̸", nscr: "𝓃", nshortmid: "∤", nshortparallel: "∦", nsim: "≁", nsime: "≄", nsimeq: "≄", nsmid: "∤", nspar: "∦", nsqsube: "⋢", nsqsupe: "⋣", nsub: "⊄", nsubE: "⫅̸", nsube: "⊈", nsubset: "⊂⃒", nsubseteq: "⊈", nsubseteqq: "⫅̸", nsucc: "⊁", nsucceq: "⪰̸", nsup: "⊅", nsupE: "⫆̸", nsupe: "⊉", nsupset: "⊃⃒", nsupseteq: "⊉", nsupseteqq: "⫆̸", ntgl: "≹", ntilde: "ñ", ntlg: "≸", ntriangleleft: "⋪", ntrianglelefteq: "⋬", ntriangleright: "⋫", ntrianglerighteq: "⋭", nu: "ν", num: "#", numero: "№", numsp: " ", nvDash: "⊭", nvHarr: "⤄", nvap: "≍⃒", nvdash: "⊬", nvge: "≥⃒", nvgt: ">⃒", nvinfin: "⧞", nvlArr: "⤂", nvle: "≤⃒", nvlt: "<⃒", nvltrie: "⊴⃒", nvrArr: "⤃", nvrtrie: "⊵⃒", nvsim: "∼⃒", nwArr: "⇖", nwarhk: "⤣", nwarr: "↖", nwarrow: "↖", nwnear: "⤧", oS: "Ⓢ", oacute: "ó", oast: "⊛", ocir: "⊚", ocirc: "ô", ocy: "о", odash: "⊝", odblac: "ő", odiv: "⨸", odot: "⊙", odsold: "⦼", oelig: "œ", ofcir: "⦿", ofr: "𝔬", ogon: "˛", ograve: "ò", ogt: "⧁", ohbar: "⦵", ohm: "Ω", oint: "∮", olarr: "↺", olcir: "⦾", olcross: "⦻", oline: "‾", olt: "⧀", omacr: "ō", omega: "ω", omicron: "ο", omid: "⦶", ominus: "⊖", oopf: "𝕠", opar: "⦷", operp: "⦹", oplus: "⊕", or: "∨", orarr: "↻", ord: "⩝", order: "ℴ", orderof: "ℴ", ordf: "ª", ordm: "º", origof: "⊶", oror: "⩖", orslope: "⩗", orv: "⩛", oscr: "ℴ", oslash: "ø", osol: "⊘", otilde: "õ", otimes: "⊗", otimesas: "⨶", ouml: "ö", ovbar: "⌽", par: "∥", para: "¶", parallel: "∥", parsim: "⫳", parsl: "⫽", part: "∂", pcy: "п", percnt: "%", period: ".", permil: "‰", perp: "⊥", pertenk: "‱", pfr: "𝔭", phi: "φ", phiv: "ϕ", phmmat: "ℳ", phone: "☎", pi: "π", pitchfork: "⋔", piv: "ϖ", planck: "ℏ", planckh: "ℎ", plankv: "ℏ", plus: "+", plusacir: "⨣", plusb: "⊞", pluscir: "⨢", plusdo: "∔", plusdu: "⨥", pluse: "⩲", plusmn: "±", plussim: "⨦", plustwo: "⨧", pm: "±", pointint: "⨕", popf: "𝕡", pound: "£", pr: "≺", prE: "⪳", prap: "⪷", prcue: "≼", pre: "⪯", prec: "≺", precapprox: "⪷", preccurlyeq: "≼", preceq: "⪯", precnapprox: "⪹", precneqq: "⪵", precnsim: "⋨", precsim: "≾", prime: "′", primes: "ℙ", prnE: "⪵", prnap: "⪹", prnsim: "⋨", prod: "∏", profalar: "⌮", profline: "⌒", profsurf: "⌓", prop: "∝", propto: "∝", prsim: "≾", prurel: "⊰", pscr: "𝓅", psi: "ψ", puncsp: " ", qfr: "𝔮", qint: "⨌", qopf: "𝕢", qprime: "⁗", qscr: "𝓆", quaternions: "ℍ", quatint: "⨖", quest: "?", questeq: "≟", quot: "\"", rAarr: "⇛", rArr: "⇒", rAtail: "⤜", rBarr: "⤏", rHar: "⥤", race: "∽̱", racute: "ŕ", radic: "√", raemptyv: "⦳", rang: "⟩", rangd: "⦒", range: "⦥", rangle: "⟩", raquo: "»", rarr: "→", rarrap: "⥵", rarrb: "⇥", rarrbfs: "⤠", rarrc: "⤳", rarrfs: "⤞", rarrhk: "↪", rarrlp: "↬", rarrpl: "⥅", rarrsim: "⥴", rarrtl: "↣", rarrw: "↝", ratail: "⤚", ratio: "∶", rationals: "ℚ", rbarr: "⤍", rbbrk: "❳", rbrace: "}", rbrack: "]", rbrke: "⦌", rbrksld: "⦎", rbrkslu: "⦐", rcaron: "ř", rcedil: "ŗ", rceil: "⌉", rcub: "}", rcy: "р", rdca: "⤷", rdldhar: "⥩", rdquo: "”", rdquor: "”", rdsh: "↳", real: "ℜ", realine: "ℛ", realpart: "ℜ", reals: "ℝ", rect: "▭", reg: "®", rfisht: "⥽", rfloor: "⌋", rfr: "𝔯", rhard: "⇁", rharu: "⇀", rharul: "⥬", rho: "ρ", rhov: "ϱ", rightarrow: "→", rightarrowtail: "↣", rightharpoondown: "⇁", rightharpoonup: "⇀", rightleftarrows: "⇄", rightleftharpoons: "⇌", rightrightarrows: "⇉", rightsquigarrow: "↝", rightthreetimes: "⋌", ring: "˚", risingdotseq: "≓", rlarr: "⇄", rlhar: "⇌", rlm: "‏", rmoust: "⎱", rmoustache: "⎱", rnmid: "⫮", roang: "⟭", roarr: "⇾", robrk: "⟧", ropar: "⦆", ropf: "𝕣", roplus: "⨮", rotimes: "⨵", rpar: ")", rpargt: "⦔", rppolint: "⨒", rrarr: "⇉", rsaquo: "›", rscr: "𝓇", rsh: "↱", rsqb: "]", rsquo: "’", rsquor: "’", rthree: "⋌", rtimes: "⋊", rtri: "▹", rtrie: "⊵", rtrif: "▸", rtriltri: "⧎", ruluhar: "⥨", rx: "℞", sacute: "ś", sbquo: "‚", sc: "≻", scE: "⪴", scap: "⪸", scaron: "š", sccue: "≽", sce: "⪰", scedil: "ş", scirc: "ŝ", scnE: "⪶", scnap: "⪺", scnsim: "⋩", scpolint: "⨓", scsim: "≿", scy: "с", sdot: "⋅", sdotb: "⊡", sdote: "⩦", seArr: "⇘", searhk: "⤥", searr: "↘", searrow: "↘", sect: "§", semi: ";", seswar: "⤩", setminus: "∖", setmn: "∖", sext: "✶", sfr: "𝔰", sfrown: "⌢", sharp: "♯", shchcy: "щ", shcy: "ш", shortmid: "∣", shortparallel: "∥", shy: "­", sigma: "σ", sigmaf: "ς", sigmav: "ς", sim: "∼", simdot: "⩪", sime: "≃", simeq: "≃", simg: "⪞", simgE: "⪠", siml: "⪝", simlE: "⪟", simne: "≆", simplus: "⨤", simrarr: "⥲", slarr: "←", smallsetminus: "∖", smashp: "⨳", smeparsl: "⧤", smid: "∣", smile: "⌣", smt: "⪪", smte: "⪬", smtes: "⪬︀", softcy: "ь", sol: "/", solb: "⧄", solbar: "⌿", sopf: "𝕤", spades: "♠", spadesuit: "♠", spar: "∥", sqcap: "⊓", sqcaps: "⊓︀", sqcup: "⊔", sqcups: "⊔︀", sqsub: "⊏", sqsube: "⊑", sqsubset: "⊏", sqsubseteq: "⊑", sqsup: "⊐", sqsupe: "⊒", sqsupset: "⊐", sqsupseteq: "⊒", squ: "□", square: "□", squarf: "▪", squf: "▪", srarr: "→", sscr: "𝓈", ssetmn: "∖", ssmile: "⌣", sstarf: "⋆", star: "☆", starf: "★", straightepsilon: "ϵ", straightphi: "ϕ", strns: "¯", sub: "⊂", subE: "⫅", subdot: "⪽", sube: "⊆", subedot: "⫃", submult: "⫁", subnE: "⫋", subne: "⊊", subplus: "⪿", subrarr: "⥹", subset: "⊂", subseteq: "⊆", subseteqq: "⫅", subsetneq: "⊊", subsetneqq: "⫋", subsim: "⫇", subsub: "⫕", subsup: "⫓", succ: "≻", succapprox: "⪸", succcurlyeq: "≽", succeq: "⪰", succnapprox: "⪺", succneqq: "⪶", succnsim: "⋩", succsim: "≿", sum: "∑", sung: "♪", sup1: "¹", sup2: "²", sup3: "³", sup: "⊃", supE: "⫆", supdot: "⪾", supdsub: "⫘", supe: "⊇", supedot: "⫄", suphsol: "⟉", suphsub: "⫗", suplarr: "⥻", supmult: "⫂", supnE: "⫌", supne: "⊋", supplus: "⫀", supset: "⊃", supseteq: "⊇", supseteqq: "⫆", supsetneq: "⊋", supsetneqq: "⫌", supsim: "⫈", supsub: "⫔", supsup: "⫖", swArr: "⇙", swarhk: "⤦", swarr: "↙", swarrow: "↙", swnwar: "⤪", szlig: "ß", target: "⌖", tau: "τ", tbrk: "⎴", tcaron: "ť", tcedil: "ţ", tcy: "т", tdot: "⃛", telrec: "⌕", tfr: "𝔱", there4: "∴", therefore: "∴", theta: "θ", thetasym: "ϑ", thetav: "ϑ", thickapprox: "≈", thicksim: "∼", thinsp: " ", thkap: "≈", thksim: "∼", thorn: "þ", tilde: "˜", times: "×", timesb: "⊠", timesbar: "⨱", timesd: "⨰", tint: "∭", toea: "⤨", top: "⊤", topbot: "⌶", topcir: "⫱", topf: "𝕥", topfork: "⫚", tosa: "⤩", tprime: "‴", trade: "™", triangle: "▵", triangledown: "▿", triangleleft: "◃", trianglelefteq: "⊴", triangleq: "≜", triangleright: "▹", trianglerighteq: "⊵", tridot: "◬", trie: "≜", triminus: "⨺", triplus: "⨹", trisb: "⧍", tritime: "⨻", trpezium: "⏢", tscr: "𝓉", tscy: "ц", tshcy: "ћ", tstrok: "ŧ", twixt: "≬", twoheadleftarrow: "↞", twoheadrightarrow: "↠", uArr: "⇑", uHar: "⥣", uacute: "ú", uarr: "↑", ubrcy: "ў", ubreve: "ŭ", ucirc: "û", ucy: "у", udarr: "⇅", udblac: "ű", udhar: "⥮", ufisht: "⥾", ufr: "𝔲", ugrave: "ù", uharl: "↿", uharr: "↾", uhblk: "▀", ulcorn: "⌜", ulcorner: "⌜", ulcrop: "⌏", ultri: "◸", umacr: "ū", uml: "¨", uogon: "ų", uopf: "𝕦", uparrow: "↑", updownarrow: "↕", upharpoonleft: "↿", upharpoonright: "↾", uplus: "⊎", upsi: "υ", upsih: "ϒ", upsilon: "υ", upuparrows: "⇈", urcorn: "⌝", urcorner: "⌝", urcrop: "⌎", uring: "ů", urtri: "◹", uscr: "𝓊", utdot: "⋰", utilde: "ũ", utri: "▵", utrif: "▴", uuarr: "⇈", uuml: "ü", uwangle: "⦧", vArr: "⇕", vBar: "⫨", vBarv: "⫩", vDash: "⊨", vangrt: "⦜", varepsilon: "ϵ", varkappa: "ϰ", varnothing: "∅", varphi: "ϕ", varpi: "ϖ", varpropto: "∝", varr: "↕", varrho: "ϱ", varsigma: "ς", varsubsetneq: "⊊︀", varsubsetneqq: "⫋︀", varsupsetneq: "⊋︀", varsupsetneqq: "⫌︀", vartheta: "ϑ", vartriangleleft: "⊲", vartriangleright: "⊳", vcy: "в", vdash: "⊢", vee: "∨", veebar: "⊻", veeeq: "≚", vellip: "⋮", verbar: "|", vert: "|", vfr: "𝔳", vltri: "⊲", vnsub: "⊂⃒", vnsup: "⊃⃒", vopf: "𝕧", vprop: "∝", vrtri: "⊳", vscr: "𝓋", vsubnE: "⫋︀", vsubne: "⊊︀", vsupnE: "⫌︀", vsupne: "⊋︀", vzigzag: "⦚", wcirc: "ŵ", wedbar: "⩟", wedge: "∧", wedgeq: "≙", weierp: "℘", wfr: "𝔴", wopf: "𝕨", wp: "℘", wr: "≀", wreath: "≀", wscr: "𝓌", xcap: "⋂", xcirc: "◯", xcup: "⋃", xdtri: "▽", xfr: "𝔵", xhArr: "⟺", xharr: "⟷", xi: "ξ", xlArr: "⟸", xlarr: "⟵", xmap: "⟼", xnis: "⋻", xodot: "⨀", xopf: "𝕩", xoplus: "⨁", xotime: "⨂", xrArr: "⟹", xrarr: "⟶", xscr: "𝓍", xsqcup: "⨆", xuplus: "⨄", xutri: "△", xvee: "⋁", xwedge: "⋀", yacute: "ý", yacy: "я", ycirc: "ŷ", ycy: "ы", yen: "¥", yfr: "𝔶", yicy: "ї", yopf: "𝕪", yscr: "𝓎", yucy: "ю", yuml: "ÿ", zacute: "ź", zcaron: "ž", zcy: "з", zdot: "ż", zeetrf: "ℨ", zeta: "ζ", zfr: "𝔷", zhcy: "ж", zigrarr: "⇝", zopf: "𝕫", zscr: "𝓏", zwj: "‍", zwnj: "‌" }; var nt = { 0: 65533, 128: 8364, 130: 8218, 131: 402, 132: 8222, 133: 8230, 134: 8224, 135: 8225, 136: 710, 137: 8240, 138: 352, 139: 8249, 140: 338, 142: 381, 145: 8216, 146: 8217, 147: 8220, 148: 8221, 149: 8226, 150: 8211, 151: 8212, 152: 732, 153: 8482, 154: 353, 155: 8250, 156: 339, 158: 382, 159: 376 }; function rt(e) { return e.replace(/&(?:[a-zA-Z]+|#[xX][\da-fA-F]+|#\d+);/g, (e) => { if (e.charAt(1) === "#") { let t = e.charAt(2); return it(t === "X" || t === "x" ? parseInt(e.slice(3), 16) : parseInt(e.slice(2), 10)); } return Ke(tt, e.slice(1, -1)) ?? e; }); } function it(e) { return e >= 55296 && e <= 57343 || e > 1114111 ? "�" : String.fromCodePoint(Ke(nt, e) ?? e); } function at(e, t) { return e.startIndex = e.tokenIndex = e.index, e.startColumn = e.tokenColumn = e.column, e.startLine = e.tokenLine = e.line, e.setToken(b$1[e.currentChar] & 8192 ? ot(e) : et(e, t, 0)), e.getToken(); } function ot(e) { let t = e.currentChar, n = _$1(e), r = e.index; for (; n !== t;) e.index >= e.end && e.report(16), n = _$1(e); return n !== t && e.report(16), e.tokenValue = e.source.slice(r, e.index), _$1(e), e.options.raw && (e.tokenRaw = e.source.slice(e.tokenIndex, e.index)), 134283267; } function j(e) { if (e.startIndex = e.tokenIndex = e.index, e.startColumn = e.tokenColumn = e.column, e.startLine = e.tokenLine = e.line, e.index >= e.end) { e.setToken(1048576); return; } if (e.currentChar === 60) { _$1(e), e.setToken(8456256); return; } if (e.currentChar === 123) { _$1(e), e.setToken(2162700); return; } let t = 0; for (; e.index < e.end;) { let n = b$1[e.source.charCodeAt(e.index)]; if (n & 1024 ? (t |= 5, v$1(e)) : n & 2048 ? (fe(e, t), t = t & -5 | 1) : _$1(e), b$1[e.currentChar] & 16384) break; } e.tokenIndex === e.index && e.report(0); let n = e.source.slice(e.tokenIndex, e.index); e.options.raw && (e.tokenRaw = n), e.tokenValue = rt(n), e.setToken(137); } function st(e) { if ((e.getToken() & 143360) == 143360) { let { index: t } = e, n = e.currentChar; for (; b$1[n] & 32770;) n = _$1(e); e.tokenValue += e.source.slice(t, e.index), e.setToken(208897, !0); } return e.getToken(); } var ct = class e { parser; type; parent; scopeError; variableBindings = /* @__PURE__ */ new Map(); constructor(e, t = 2, n) { this.parser = e, this.type = t, this.parent = n; } createChildScope(t) { return new e(this.parser, t, this); } addVarOrBlock(e, t, n, r) { n & 4 ? this.addVarName(e, t, n) : this.addBlockName(e, t, n, r), r & 64 && this.parser.declareUnboundVariable(t); } addVarName(e, t, n) { let { parser: r } = this, i = this; for (; i && !(i.type & 128);) { let { variableBindings: a } = i, o = a.get(t); o && o & 248 && (r.options.webcompat && !(e & 1) && (n & 128 && o & 68 || o & 128 && n & 68) || r.report(145, t)), i === this && o && o & 1 && n & 1 && i.recordScopeError(145, t), o && (o & 256 || o & 512 && !r.options.webcompat) && r.report(145, t), i.variableBindings.set(t, n), i = i.parent; } } hasVariable(e) { return this.variableBindings.has(e); } addBlockName(e, t, n, r) { let { parser: i } = this, a = this.variableBindings.get(t); a && !(a & 2) && (n & 1 ? this.recordScopeError(145, t) : i.options.webcompat && !(e & 1) && r & 2 && a === 64 && n === 64 || i.report(145, t)), this.type & 64 && this.parent?.hasVariable(t) && !(this.parent.variableBindings.get(t) & 2) && i.report(145, t), this.type & 512 && a && !(a & 2) && n & 1 && this.recordScopeError(145, t), this.type & 32 && this.parent.variableBindings.get(t) & 768 && i.report(159, t), this.variableBindings.set(t, n); } recordScopeError(e, ...t) { this.scopeError = { type: e, params: t, start: this.parser.tokenStart, end: this.parser.currentLocation }; } reportScopeError() { let { scopeError: e } = this; if (e) throw new C$1(e.start, e.end, e.type, ...e.params); } }; function lt(e, t, n) { let r = e.createScope().createChildScope(512); return r.addBlockName(t, n, 1, 0), r; } var ut = class { parser; parent; refs = Object.create(null); privateIdentifiers = /* @__PURE__ */ new Map(); constructor(e, t) { this.parser = e, this.parent = t; } addPrivateIdentifier(e, t) { let { privateIdentifiers: n } = this, r = t & 800; r & 768 || (r |= 768); let i = n.get(e); this.hasPrivateIdentifier(e) && ((i & 32) != (r & 32) || i & r & 768) && this.parser.report(146, e), n.set(e, this.hasPrivateIdentifier(e) ? i | r : r); } addPrivateIdentifierRef(e) { this.refs[e] ??= [], this.refs[e].push(this.parser.tokenStart); } isPrivateIdentifierDefined(e) { return this.hasPrivateIdentifier(e) || !!this.parent?.isPrivateIdentifierDefined(e); } validatePrivateIdentifierRefs() { for (let e in this.refs) if (!this.isPrivateIdentifierDefined(e)) { let { index: t, line: n, column: r } = this.refs[e][0]; throw new C$1({ index: t, line: n, column: r }, { index: t + e.length, line: n, column: r + e.length }, 4, e); } } hasPrivateIdentifier(e) { return this.privateIdentifiers.has(e); } }; var dt = class { source; options; lastOnToken = null; token = 1048576; flags = 0; index = 0; line = 1; column = 0; startIndex = 0; end = 0; tokenIndex = 0; startColumn = 0; tokenColumn = 0; tokenLine = 1; startLine = 1; tokenValue = ""; tokenRaw = ""; tokenRegExp = void 0; currentChar = 0; exportedNames = /* @__PURE__ */ new Set(); exportedBindings = /* @__PURE__ */ new Set(); assignable = 1; destructible = 0; leadingDecorators = { decorators: [] }; constructor(e, t = {}) { this.source = e, this.options = t, this.end = e.length, this.currentChar = e.charCodeAt(0); } getToken() { return this.token; } setToken(e, t = !1) { this.token = e; let { onToken: n } = this.options; if (n) if (e !== 1048576) { let r = { start: { line: this.tokenLine, column: this.tokenColumn }, end: { line: this.line, column: this.column } }; !t && this.lastOnToken && n(...this.lastOnToken), this.lastOnToken = [ me(e), this.tokenIndex, this.index, r ]; } else this.lastOnToken &&= (n(...this.lastOnToken), null); return e; } get tokenStart() { return { index: this.tokenIndex, line: this.tokenLine, column: this.tokenColumn }; } get currentLocation() { return { index: this.index, line: this.line, column: this.column }; } finishNode(e, t, n) { if (this.options.ranges) { e.start = t.index; let r = n ? n.index : this.startIndex; e.end = r, e.range = [t.index, r]; } return this.options.loc && (e.loc = { start: { line: t.line, column: t.column }, end: n ? { line: n.line, column: n.column } : { line: this.startLine, column: this.startColumn } }, this.options.source && (e.loc.source = this.options.source)), e; } addBindingToExports(e) { this.exportedBindings.add(e); } declareUnboundVariable(e) { let { exportedNames: t } = this; t.has(e) && this.report(147, e), t.add(e); } report(e, ...t) { throw new C$1(this.tokenStart, this.currentLocation, e, ...t); } createScopeIfLexical(e, t) { if (this.options.lexical) return this.createScope(e, t); } createScope(e, t) { return new ct(this, e, t); } createPrivateScopeIfLexical(e) { if (this.options.lexical) return new ut(this, e); } }; function ft(e, t) { return function(n, r, i, a, o) { let s = { type: n, value: r }; t.ranges && (s.start = i, s.end = a, s.range = [i, a]), t.loc && (s.loc = o), e.push(s); }; } function pt(e, t) { return function(n, r, i, a) { let o = { token: n }; t.ranges && (o.start = r, o.end = i, o.range = [r, i]), t.loc && (o.loc = a), e.push(o); }; } function mt(e) { let t = { ...e }; return t.onComment &&= Array.isArray(t.onComment) ? ft(t.onComment, t) : t.onComment, t.onToken &&= Array.isArray(t.onToken) ? pt(t.onToken, t) : t.onToken, t; } function ht(e, t = {}, n = 0) { let r = mt(t); r.module && (n |= 3), r.globalReturn && (n |= 4096), r.impliedStrict && (n |= 1); let i = new dt(e, r); be(i); let a = i.createScopeIfLexical(), o = [], s = "script"; if (n & 2) { if (s = "module", o = _t(i, n | 8, a), a) for (let e of i.exportedBindings) a.hasVariable(e) || i.report(148, e); } else o = gt(i, n | 8, a); return i.finishNode({ type: "Program", sourceType: s, body: o }, { index: 0, line: 1, column: 0 }, i.currentLocation); } function gt(e, t, n) { A$1(e, t | 262176); let r = []; for (; e.getToken() === 134283267;) { let { index: n, tokenValue: i, tokenStart: a, tokenIndex: o } = e, s = e.getToken(), c = q(e, t); if (Ie(e, n, o, i)) { if (t |= 1, e.flags & 64) throw new C$1(e.tokenStart, e.currentLocation, 9); if (e.flags & 4096) throw new C$1(e.tokenStart, e.currentLocation, 15); } r.push(wt(e, t, c, s, a)); } for (; e.getToken() !== 1048576;) r.push(M$1(e, t, n, void 0, 4, {})); return r; } function _t(e, t, n) { A$1(e, t | 32); let r = []; for (; e.getToken() === 134283267;) { let { tokenStart: n } = e, i = e.getToken(); r.push(wt(e, t, q(e, t), i, n)); } for (; e.getToken() !== 1048576;) r.push(vt(e, t, n)); return r; } function vt(e, t, n) { e.getToken() === 132 && Object.assign(e.leadingDecorators, { start: e.tokenStart, decorators: Gn(e, t, void 0) }); let r; switch (e.getToken()) { case 20564: r = Zt(e, t, n); break; case 86106: r = Gt(e, t, n); break; default: r = M$1(e, t, n, void 0, 4, {}); } return e.leadingDecorators?.decorators.length && e.report(170), r; } function M$1(e, t, n, r, i, a) { let o = e.tokenStart; switch (e.getToken()) { case 86104: return J(e, t, n, r, i, 1, 0, 0, o); case 132: case 86094: return Un(e, t, n, r, 0); case 86090: return Bt(e, t, n, r, 16, 0); case 241737: return zt(e, t, n, r, i); case 20564: e.report(103, "export"); case 86106: switch (A$1(e, t), e.getToken()) { case 67174411: return Xt(e, t, r, o); case 67108877: return Yt(e, t, o); default: e.report(103, "import"); } case 209005: return Ct(e, t, n, r, i, a, 1); default: return N$1(e, t, n, r, i, a, 1); } } function N$1(e, t, n, r, i, a, o) { switch (e.getToken()) { case 86088: return Vt(e, t, n, r, 0); case 20572: return xt(e, t, r); case 20569: return Dt(e, t, n, r, a); case 20567: return Ut(e, t, n, r, a); case 20562: return Rt(e, t, n, r, a); case 20578: return At(e, t, n, r, a); case 86110: return kt(e, t, n, r, a); case 1074790417: return Tt(e, t); case 2162700: return bt(e, t, n?.createChildScope(), r, a, e.tokenStart); case 86112: return Et(e, t, r); case 20555: return Mt(e, t, a); case 20559: return jt(e, t, a); case 20577: return Ft(e, t, n, r, a); case 20579: return Nt(e, t, n, r, a); case 20560: return Pt(e, t); case 209005: return Ct(e, t, n, r, i, a, 0); case 20557: e.report(162); case 20566: e.report(163); case 86104: e.report(t & 1 ? 76 : e.options.webcompat ? 77 : 78); case 86094: e.report(79); default: return yt$1(e, t, n, r, i, a, o); } } function yt$1(e, t, n, r, i, a, o) { let { tokenValue: s, tokenStart: c } = e, l = e.getToken(), u; switch (l) { case 241737: u = K(e, t), t & 1 && e.report(85), e.getToken() === 69271571 && e.report(84); break; default: u = G(e, t, r, 2, 0, 1, 0, 1, e.tokenStart); } return l & 143360 && e.getToken() === 21 ? St(e, t, n, r, i, a, s, u, l, o, c) : (u = W(e, t, r, u, 0, 0, c), u = B$1(e, t, r, 0, 0, c, u), e.getToken() === 18 && (u = R$1(e, t, r, 0, c, u)), P$1(e, t, u, c)); } function bt(e, t, n, r, i, a = e.tokenStart, o = "BlockStatement") { let s = []; for (D$1(e, t | 32, 2162700); e.getToken() !== 1074790415;) s.push(M$1(e, t, n, r, 2, { $: i })); return D$1(e, t | 32, 1074790415), e.finishNode({ type: o, body: s }, a); } function xt(e, t, n) { t & 4096 || e.report(92); let r = e.tokenStart; A$1(e, t | 32); let i = e.flags & 1 || e.getToken() & 1048576 ? null : z$1(e, t, n, 0, 1, e.tokenStart); return T$1(e, t | 32), e.finishNode({ type: "ReturnStatement", argument: i }, r); } function P$1(e, t, n, r) { return T$1(e, t | 32), e.finishNode({ type: "ExpressionStatement", expression: n }, r); } function St(e, t, n, r, i, a, o, s, c, l, u) { Re(e, t, 0, c, 1), Ue(e, a, o), A$1(e, t | 32); let d = l && !(t & 1) && e.options.webcompat && e.getToken() === 86104 ? J(e, t, n?.createChildScope(), r, i, 0, 0, 0, e.tokenStart) : N$1(e, t, n, r, i, a, l); return e.finishNode({ type: "LabeledStatement", label: s, body: d }, u); } function Ct(e, t, n, r, i, a, o) { let { tokenValue: s, tokenStart: c } = e, l = e.getToken(), u = K(e, t); if (e.getToken() === 21) return St(e, t, n, r, i, a, s, u, l, 1, c); let d = e.flags & 1; if (!d) { if (e.getToken() === 86104) return o || e.report(123), J(e, t, n, r, i, 1, 0, 1, c); if (k$1(t, e.getToken())) return u = Bn(e, t, r, 1, c), e.getToken() === 18 && (u = R$1(e, t, r, 0, c, u)), P$1(e, t, u, c); } return e.getToken() === 67174411 ? u = Vn(e, t, r, u, 1, 1, 0, d, c) : (e.getToken() === 10 && (Ge(e, t, l), (l & 36864) == 36864 && (e.flags |= 256), u = Nn(e, t | 2048, r, e.tokenValue, u, 0, 1, 0, c)), e.assignable = 1), u = W(e, t, r, u, 0, 0, c), u = B$1(e, t, r, 0, 0, c, u), e.assignable = 1, e.getToken() === 18 && (u = R$1(e, t, r, 0, c, u)), P$1(e, t, u, c); } function wt(e, t, n, r, i) { let a = e.startIndex; r !== 1074790417 && (e.assignable = 2, n = W(e, t, void 0, n, 0, 0, i), e.getToken() !== 1074790417 && (n = B$1(e, t, void 0, 0, 0, i, n), e.getToken() === 18 && (n = R$1(e, t, void 0, 0, i, n))), T$1(e, t | 32)); let o = { type: "ExpressionStatement", expression: n }; return n.type === "Literal" && typeof n.value == "string" && (o.directive = e.source.slice(i.index + 1, a - 1)), e.finishNode(o, i); } function Tt(e, t) { let n = e.tokenStart; return A$1(e, t | 32), e.finishNode({ type: "EmptyStatement" }, n); } function Et(e, t, n) { let r = e.tokenStart; A$1(e, t | 32), e.flags & 1 && e.report(90); let i = z$1(e, t, n, 0, 1, e.tokenStart); return T$1(e, t | 32), e.finishNode({ type: "ThrowStatement", argument: i }, r); } function Dt(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t), D$1(e, t | 32, 67174411), e.assignable = 1; let o = z$1(e, t, r, 0, 1, e.tokenStart); D$1(e, t | 32, 16); let s = Ot(e, t, n, r, i), c = null; return e.getToken() === 20563 && (A$1(e, t | 32), c = Ot(e, t, n, r, i)), e.finishNode({ type: "IfStatement", test: o, consequent: s, alternate: c }, a); } function Ot(e, t, n, r, i) { let { tokenStart: a } = e; return t & 1 || !e.options.webcompat || e.getToken() !== 86104 ? N$1(e, t, n, r, 0, { $: i }, 0) : J(e, t, n?.createChildScope(), r, 0, 0, 0, 0, a); } function kt(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t), D$1(e, t | 32, 67174411); let o = z$1(e, t, r, 0, 1, e.tokenStart); D$1(e, t, 16), D$1(e, t, 2162700); let s = [], c = 0; for (n = n?.createChildScope(8); e.getToken() !== 1074790415;) { let { tokenStart: a } = e, o = null, l = []; for (E$1(e, t | 32, 20556) ? o = z$1(e, t, r, 0, 1, e.tokenStart) : (D$1(e, t | 32, 20561), c && e.report(89), c = 1), D$1(e, t | 32, 21); e.getToken() !== 20556 && e.getToken() !== 1074790415 && e.getToken() !== 20561;) l.push(M$1(e, t | 4, n, r, 2, { $: i })); s.push(e.finishNode({ type: "SwitchCase", test: o, consequent: l }, a)); } return D$1(e, t | 32, 1074790415), e.finishNode({ type: "SwitchStatement", discriminant: o, cases: s }, a); } function At(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t), D$1(e, t | 32, 67174411); let o = z$1(e, t, r, 0, 1, e.tokenStart); D$1(e, t | 32, 16); let s = F(e, t, n, r, i); return e.finishNode({ type: "WhileStatement", test: o, body: s }, a); } function F(e, t, n, r, i) { return N$1(e, (t | 131072) ^ 131072 | 128, n, r, 0, { loop: 1, $: i }, 0); } function jt(e, t, n) { t & 128 || e.report(68); let r = e.tokenStart; A$1(e, t); let i = null; if (!(e.flags & 1) && e.getToken() & 143360) { let { tokenValue: r } = e; i = K(e, t | 32), He(e, n, r, 1) || e.report(138, r); } return T$1(e, t | 32), e.finishNode({ type: "ContinueStatement", label: i }, r); } function Mt(e, t, n) { let r = e.tokenStart; A$1(e, t | 32); let i = null; if (!(e.flags & 1) && e.getToken() & 143360) { let { tokenValue: r } = e; i = K(e, t | 32), He(e, n, r, 0) || e.report(138, r); } else t & 132 || e.report(69); return T$1(e, t | 32), e.finishNode({ type: "BreakStatement", label: i }, r); } function Nt(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t), t & 1 && e.report(91), D$1(e, t | 32, 67174411); let o = z$1(e, t, r, 0, 1, e.tokenStart); D$1(e, t | 32, 16); let s = N$1(e, t, n, r, 2, i, 0); return e.finishNode({ type: "WithStatement", object: o, body: s }, a); } function Pt(e, t) { let n = e.tokenStart; return A$1(e, t | 32), T$1(e, t | 32), e.finishNode({ type: "DebuggerStatement" }, n); } function Ft(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t | 32); let o = n?.createChildScope(16), s = bt(e, t, o, r, { $: i }), { tokenStart: c } = e, l = E$1(e, t | 32, 20557) ? It(e, t, n, r, i, c) : null, u = null; if (e.getToken() === 20566) { A$1(e, t | 32); let a = n?.createChildScope(4); u = bt(e, t, a, r, { $: i }); } return !l && !u && e.report(88), e.finishNode({ type: "TryStatement", block: s, handler: l, finalizer: u }, a); } function It(e, t, n, r, i, a) { let o = null, s = n; E$1(e, t, 67174411) && (n = n?.createChildScope(4), o = Zn(e, t, n, r, (e.getToken() & 2097152) == 2097152 ? 256 : 512, 0), e.getToken() === 18 ? e.report(86) : e.getToken() === 1077936155 && e.report(87), D$1(e, t | 32, 16)), s = n?.createChildScope(32); let c = bt(e, t, s, r, { $: i }); return e.finishNode({ type: "CatchClause", param: o, body: c }, a); } function Lt(e, t, n, r, i) { n = n?.createChildScope(); let a = 5764; return t = (t | a) ^ a | 592128, bt(e, t, n, r, {}, i, "StaticBlock"); } function Rt(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t | 32); let o = F(e, t, n, r, i); D$1(e, t, 20578), D$1(e, t | 32, 67174411); let s = z$1(e, t, r, 0, 1, e.tokenStart); return D$1(e, t | 32, 16), E$1(e, t | 32, 1074790417), e.finishNode({ type: "DoWhileStatement", body: o, test: s }, a); } function zt(e, t, n, r, i) { let { tokenValue: a, tokenStart: o } = e, s = e.getToken(), c = K(e, t); if (e.getToken() & 2240512) { let i = I$1(e, t, n, r, 8, 0); return T$1(e, t | 32), e.finishNode({ type: "VariableDeclaration", kind: "let", declarations: i }, o); } if (e.assignable = 1, t & 1 && e.report(85), e.getToken() === 21) return St(e, t, n, r, i, {}, a, c, s, 0, o); if (e.getToken() === 10) { let n; e.options.lexical && (n = lt(e, t, a)), e.flags = (e.flags | 128) ^ 128, c = Fn(e, t, n, r, [c], 0, o); } else c = W(e, t, r, c, 0, 0, o), c = B$1(e, t, r, 0, 0, o, c); return e.getToken() === 18 && (c = R$1(e, t, r, 0, o, c)), P$1(e, t, c, o); } function Bt(e, t, n, r, i, a) { let o = e.tokenStart; A$1(e, t); let s = I$1(e, t, n, r, i, a); return T$1(e, t | 32), e.finishNode({ type: "VariableDeclaration", kind: i & 8 ? "let" : "const", declarations: s }, o); } function Vt(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t); let o = I$1(e, t, n, r, 4, i); return T$1(e, t | 32), e.finishNode({ type: "VariableDeclaration", kind: "var", declarations: o }, a); } function I$1(e, t, n, r, i, a) { let o = 1, s = [Ht(e, t, n, r, i, a)]; for (; E$1(e, t, 18);) o++, s.push(Ht(e, t, n, r, i, a)); return o > 1 && a & 32 && e.getToken() & 262144 && e.report(61, w$1[e.getToken() & 255]), s; } function Ht(e, t, n, r, i, a) { let { tokenStart: o } = e, s = e.getToken(), c = null, l = Zn(e, t, n, r, i, a); if (e.getToken() === 1077936155) { if (A$1(e, t | 32), c = L$1(e, t, r, 1, 0, e.tokenStart), (a & 32 || !(s & 2097152)) && (e.getToken() === 471156 || e.getToken() === 8673330 && (s & 2097152 || !(i & 4) || t & 1))) throw new C$1(o, e.currentLocation, 60, e.getToken() === 471156 ? "of" : "in"); } else (i & 16 || (s & 2097152) > 0) && (e.getToken() & 262144) != 262144 && e.report(59, i & 16 ? "const" : "destructuring"); return e.finishNode({ type: "VariableDeclarator", id: l, init: c }, o); } function Ut(e, t, n, r, i) { let a = e.tokenStart; A$1(e, t); let o = ((t & 2048) > 0 || (t & 2) > 0 && (t & 8) > 0) && E$1(e, t, 209006); D$1(e, t | 32, 67174411), n = n?.createChildScope(1); let s = null, c = null, l = 0, u = null, d = e.getToken() === 86088 || e.getToken() === 241737 || e.getToken() === 86090, f, { tokenStart: p } = e, m = e.getToken(); if (d) m === 241737 ? (u = K(e, t), e.getToken() & 2240512 ? (e.getToken() === 8673330 ? t & 1 && e.report(67) : u = e.finishNode({ type: "VariableDeclaration", kind: "let", declarations: I$1(e, t | 131072, n, r, 8, 32) }, p), e.assignable = 1) : t & 1 ? e.report(67) : (d = !1, e.assignable = 1, u = W(e, t, r, u, 0, 0, p), e.getToken() === 471156 && e.report(115))) : (A$1(e, t), u = e.finishNode(m === 86088 ? { type: "VariableDeclaration", kind: "var", declarations: I$1(e, t | 131072, n, r, 4, 32) } : { type: "VariableDeclaration", kind: "const", declarations: I$1(e, t | 131072, n, r, 16, 32) }, p), e.assignable = 1); else if (m === 1074790417) o && e.report(82); else if ((m & 2097152) == 2097152) { let n = e.tokenStart; u = m === 2162700 ? Q(e, t, void 0, r, 1, 0, 0, 2, 32) : Y(e, t, void 0, r, 1, 0, 0, 2, 32), l = e.destructible, l & 64 && e.report(63), e.assignable = l & 16 ? 2 : 1, u = W(e, t | 131072, r, u, 0, 0, n); } else u = U(e, t | 131072, r, 1, 0, 1); if ((e.getToken() & 262144) == 262144) { if (e.getToken() === 471156) { e.assignable & 2 && e.report(80, o ? "await" : "of"), O(e, u), A$1(e, t | 32), f = L$1(e, t, r, 1, 0, e.tokenStart), D$1(e, t | 32, 16); let s = F(e, t, n, r, i); return e.finishNode({ type: "ForOfStatement", left: u, right: f, body: s, await: o }, a); } e.assignable & 2 && e.report(80, "in"), O(e, u), A$1(e, t | 32), o && e.report(82), f = z$1(e, t, r, 0, 1, e.tokenStart), D$1(e, t | 32, 16); let s = F(e, t, n, r, i); return e.finishNode({ type: "ForInStatement", body: s, left: u, right: f }, a); } o && e.report(82), d || (l & 8 && e.getToken() !== 1077936155 && e.report(80, "loop"), u = B$1(e, t | 131072, r, 0, 0, p, u)), e.getToken() === 18 && (u = R$1(e, t, r, 0, p, u)), D$1(e, t | 32, 1074790417), e.getToken() !== 1074790417 && (s = z$1(e, t, r, 0, 1, e.tokenStart)), D$1(e, t | 32, 1074790417), e.getToken() !== 16 && (c = z$1(e, t, r, 0, 1, e.tokenStart)), D$1(e, t | 32, 16); let h = F(e, t, n, r, i); return e.finishNode({ type: "ForStatement", init: u, test: s, update: c, body: h }, a); } function Wt(e, t, n) { return k$1(t, e.getToken()) || e.report(118), (e.getToken() & 537079808) == 537079808 && e.report(119), n?.addBlockName(t, e.tokenValue, 8, 0), K(e, t); } function Gt(e, t, n) { let r = e.tokenStart; A$1(e, t); let i = null, { tokenStart: a } = e, o = []; if (e.getToken() === 134283267) i = q(e, t); else { if (e.getToken() & 143360) { let r = Wt(e, t, n); if (o = [e.finishNode({ type: "ImportDefaultSpecifier", local: r }, a)], E$1(e, t, 18)) switch (e.getToken()) { case 8391476: o.push(Kt(e, t, n)); break; case 2162700: Jt(e, t, n, o); break; default: e.report(107); } } else switch (e.getToken()) { case 8391476: o = [Kt(e, t, n)]; break; case 2162700: Jt(e, t, n, o); break; case 67174411: return Xt(e, t, void 0, r); case 67108877: return Yt(e, t, r); default: e.report(30, w$1[e.getToken() & 255]); } i = qt(e, t); } let s = pn(e, t), c = { type: "ImportDeclaration", specifiers: o, source: i, attributes: s }; return T$1(e, t | 32), e.finishNode(c, r); } function Kt(e, t, n) { let { tokenStart: r } = e; if (A$1(e, t), D$1(e, t, 77932), (e.getToken() & 134217728) == 134217728) throw new C$1(r, e.currentLocation, 30, w$1[e.getToken() & 255]); return e.finishNode({ type: "ImportNamespaceSpecifier", local: Wt(e, t, n) }, r); } function qt(e, t) { return D$1(e, t, 209011), e.getToken() !== 134283267 && e.report(105, "Import"), q(e, t); } function Jt(e, t, n, r) { for (A$1(e, t); e.getToken() & 143360 || e.getToken() === 134283267;) { let { tokenValue: i, tokenStart: a } = e, o = e.getToken(), s = _n(e, t), c; E$1(e, t, 77932) ? ((e.getToken() & 134217728) == 134217728 || e.getToken() === 18 ? e.report(106) : Re(e, t, 16, e.getToken(), 0), i = e.tokenValue, c = K(e, t)) : s.type === "Identifier" ? (Re(e, t, 16, o, 0), c = s) : e.report(25, w$1[108]), n?.addBlockName(t, i, 8, 0), r.push(e.finishNode({ type: "ImportSpecifier", local: c, imported: s }, a)), e.getToken() !== 1074790415 && D$1(e, t, 18); } return D$1(e, t, 1074790415), r; } function Yt(e, t, n) { let r = dn(e, t, e.finishNode({ type: "Identifier", name: "import" }, n), n); return r = W(e, t, void 0, r, 0, 0, n), r = B$1(e, t, void 0, 0, 0, n, r), e.getToken() === 18 && (r = R$1(e, t, void 0, 0, n, r)), P$1(e, t, r, n); } function Xt(e, t, n, r) { let i = fn(e, t, n, 0, r); return i = W(e, t, n, i, 0, 0, r), e.getToken() === 18 && (i = R$1(e, t, n, 0, r, i)), P$1(e, t, i, r); } function Zt(e, t, n) { let r = e.leadingDecorators.decorators.length ? e.leadingDecorators.start : e.tokenStart; A$1(e, t | 32); let i = [], a = null, o = null, s = []; if (E$1(e, t | 32, 20561)) { switch (e.getToken()) { case 86104: a = J(e, t, n, void 0, 4, 1, 1, 0, e.tokenStart); break; case 132: case 86094: a = Un(e, t, n, void 0, 1); break; case 209005: { let { tokenStart: r } = e; a = K(e, t); let { flags: i } = e; i & 1 || (e.getToken() === 86104 ? a = J(e, t, n, void 0, 4, 1, 1, 1, r) : e.getToken() === 67174411 ? (a = Vn(e, t, void 0, a, 1, 1, 0, i, r), a = W(e, t, void 0, a, 0, 0, r), a = B$1(e, t, void 0, 0, 0, r, a)) : e.getToken() & 143360 && (n &&= lt(e, t, e.tokenValue), a = K(e, t), a = Fn(e, t, n, void 0, [a], 1, r))); break; } default: a = L$1(e, t, void 0, 1, 0, e.tokenStart), T$1(e, t | 32); } return n && e.declareUnboundVariable("default"), e.finishNode({ type: "ExportDefaultDeclaration", declaration: a }, r); } switch (e.getToken()) { case 8391476: { A$1(e, t); let i = null; E$1(e, t, 77932) && (n && e.declareUnboundVariable(e.tokenValue), i = _n(e, t)), D$1(e, t, 209011), e.getToken() !== 134283267 && e.report(105, "Export"), o = q(e, t); let a = pn(e, t), s = { type: "ExportAllDeclaration", source: o, exported: i, attributes: a }; return T$1(e, t | 32), e.finishNode(s, r); } case 2162700: { A$1(e, t); let r = [], a = [], c = 0; for (; e.getToken() & 143360 || e.getToken() === 134283267;) { let { tokenStart: o, tokenValue: s } = e, l = _n(e, t); l.type === "Literal" && (c = 1); let u; e.getToken() === 77932 ? (A$1(e, t), !(e.getToken() & 143360) && e.getToken() !== 134283267 && e.report(106), n && (r.push(e.tokenValue), a.push(s)), u = _n(e, t)) : (n && (r.push(e.tokenValue), a.push(e.tokenValue)), u = l), i.push(e.finishNode({ type: "ExportSpecifier", local: l, exported: u }, o)), e.getToken() !== 1074790415 && D$1(e, t, 18); } D$1(e, t, 1074790415), E$1(e, t, 209011) ? (e.getToken() !== 134283267 && e.report(105, "Export"), o = q(e, t), s = pn(e, t), n && r.forEach((t) => e.declareUnboundVariable(t))) : (c && e.report(172), n && (r.forEach((t) => e.declareUnboundVariable(t)), a.forEach((t) => e.addBindingToExports(t)))), T$1(e, t | 32); break; } case 132: case 86094: a = Un(e, t, n, void 0, 2); break; case 86104: a = J(e, t, n, void 0, 4, 1, 2, 0, e.tokenStart); break; case 241737: a = Bt(e, t, n, void 0, 8, 64); break; case 86090: a = Bt(e, t, n, void 0, 16, 64); break; case 86088: a = Vt(e, t, n, void 0, 64); break; case 209005: { let { tokenStart: r } = e; if (A$1(e, t), !(e.flags & 1) && e.getToken() === 86104) { a = J(e, t, n, void 0, 4, 1, 2, 1, r); break; } } default: e.report(30, w$1[e.getToken() & 255]); } let c = { type: "ExportNamedDeclaration", declaration: a, specifiers: i, source: o, attributes: s }; return e.finishNode(c, r); } function L$1(e, t, n, r, i, a) { let o = G(e, t, n, 2, 0, r, i, 1, a); return o = W(e, t, n, o, i, 0, a), B$1(e, t, n, i, 0, a, o); } function R$1(e, t, n, r, i, a) { let o = [a]; for (; E$1(e, t | 32, 18);) o.push(L$1(e, t, n, 1, r, e.tokenStart)); return e.finishNode({ type: "SequenceExpression", expressions: o }, i); } function z$1(e, t, n, r, i, a) { let o = L$1(e, t, n, i, r, a); return e.getToken() === 18 ? R$1(e, t, n, r, a, o) : o; } function B$1(e, t, n, r, i, a, o) { let s = e.getToken(); if ((s & 4194304) == 4194304) { e.assignable & 2 && e.report(26), (!i && s === 1077936155 && o.type === "ArrayExpression" || o.type === "ObjectExpression") && O(e, o), A$1(e, t | 32); let c = L$1(e, t, n, 1, r, e.tokenStart); return e.assignable = 2, e.finishNode(i ? { type: "AssignmentPattern", left: o, right: c } : { type: "AssignmentExpression", left: o, operator: w$1[s & 255], right: c }, a); } return (s & 8388608) == 8388608 && (o = H$1(e, t, n, r, a, 4, s, o)), E$1(e, t | 32, 22) && (o = V$1(e, t, n, o, a)), o; } function Qt(e, t, n, r, i, a, o) { let s = e.getToken(); A$1(e, t | 32); let c = L$1(e, t, n, 1, r, e.tokenStart); return o = e.finishNode(i ? { type: "AssignmentPattern", left: o, right: c } : { type: "AssignmentExpression", left: o, operator: w$1[s & 255], right: c }, a), e.assignable = 2, o; } function V$1(e, t, n, r, i) { let a = L$1(e, (t | 131072) ^ 131072, n, 1, 0, e.tokenStart); D$1(e, t | 32, 21), e.assignable = 1; let o = L$1(e, t, n, 1, 0, e.tokenStart); return e.assignable = 2, e.finishNode({ type: "ConditionalExpression", test: r, consequent: a, alternate: o }, i); } function H$1(e, t, n, r, i, a, o, s) { let c = -((t & 131072) > 0) & 8673330, l, u; for (e.assignable = 2; e.getToken() & 8388608 && (l = e.getToken(), u = l & 3840, (l & 524288 && o & 268435456 || o & 524288 && l & 268435456) && e.report(165), !(u + ((l === 8391735) << 8) - ((c === l) << 12) <= a));) A$1(e, t | 32), s = e.finishNode({ type: l & 524288 || l & 268435456 ? "LogicalExpression" : "BinaryExpression", left: s, right: H$1(e, t, n, r, e.tokenStart, u, l, U(e, t, n, 0, r, 1)), operator: w$1[l & 255] }, i); return e.getToken() === 1077936155 && e.report(26), s; } function $t(e, t, n, r, i) { r || e.report(0); let { tokenStart: a } = e, o = e.getToken(); A$1(e, t | 32); let s = U(e, t, n, 0, i, 1); return e.getToken() === 8391735 && e.report(33), t & 1 && o === 16863276 && (s.type === "Identifier" ? e.report(121) : Ve(s) && e.report(127)), e.assignable = 2, e.finishNode({ type: "UnaryExpression", operator: w$1[o & 255], argument: s, prefix: !0 }, a); } function en(e, t, n, r, i, a, o, s) { let c = e.getToken(), l = K(e, t), { flags: u } = e; if (!(u & 1)) { if (e.getToken() === 86104) return En(e, t, n, 1, r, s); if (k$1(t, e.getToken())) return i || e.report(0), (e.getToken() & 36864) == 36864 && (e.flags |= 256), Bn(e, t, n, a, s); } return !o && e.getToken() === 67174411 ? Vn(e, t, n, l, a, 1, 0, u, s) : e.getToken() === 10 ? (Ge(e, t, c), o && e.report(51), (c & 36864) == 36864 && (e.flags |= 256), Nn(e, t, n, e.tokenValue, l, o, a, 0, s)) : (e.assignable = 1, l); } function tn(e, t, n, r, i, a) { if (r && (e.destructible |= 256), t & 1024) { A$1(e, t | 32), t & 8192 && e.report(32), i || e.report(26), e.getToken() === 22 && e.report(124); let r = null, o = !1; return e.flags & 1 ? e.getToken() === 8391476 && e.report(30, w$1[e.getToken() & 255]) : (o = E$1(e, t | 32, 8391476), (e.getToken() & 77824 || o) && (r = L$1(e, t, n, 1, 0, e.tokenStart))), e.assignable = 2, e.finishNode({ type: "YieldExpression", argument: r, delegate: o }, a); } return t & 1 && e.report(97, "yield"), Mn(e, t, n); } function nn(e, t, n, r, i, a) { i && (e.destructible |= 128), t & 524288 && e.report(177); let o = Mn(e, t, n); if (o.type === "ArrowFunctionExpression" || !(e.getToken() & 65536)) { if (t & 2048) throw new C$1(a, { index: e.startIndex, line: e.startLine, column: e.startColumn }, 176); if (t & 2 || t & 8192 && t & 2048) throw new C$1(a, { index: e.startIndex, line: e.startLine, column: e.startColumn }, 110); return o; } if (t & 8192) throw new C$1(a, { index: e.startIndex, line: e.startLine, column: e.startColumn }, 31); if (t & 2048 || t & 2 && t & 8) { if (r) throw new C$1(a, { index: e.startIndex, line: e.startLine, column: e.startColumn }, 0); let i = U(e, t, n, 0, 0, 1); return e.getToken() === 8391735 && e.report(33), e.assignable = 2, e.finishNode({ type: "AwaitExpression", argument: i }, a); } if (t & 2) throw new C$1(a, { index: e.startIndex, line: e.startLine, column: e.startColumn }, 98); return o; } function rn(e, t, n, r, i, a, o) { let { tokenStart: s } = e; D$1(e, t | 32, 2162700); let c = []; if (e.getToken() !== 1074790415) { for (; e.getToken() === 134283267;) { let { index: n, tokenStart: r, tokenIndex: i, tokenValue: a } = e, s = e.getToken(), l = q(e, t); if (Ie(e, n, i, a)) { if (t |= 1, e.flags & 128) throw new C$1(r, e.currentLocation, 66); if (e.flags & 64) throw new C$1(r, e.currentLocation, 9); if (e.flags & 4096) throw new C$1(r, e.currentLocation, 15); o?.reportScopeError(); } c.push(wt(e, t, l, s, r)); } t & 1 && (a && ((a & 537079808) == 537079808 && e.report(119), (a & 36864) == 36864 && e.report(40)), e.flags & 512 && e.report(119), e.flags & 256 && e.report(118)); } for (e.flags = (e.flags | 4928) ^ 4928, e.destructible = (e.destructible | 256) ^ 256; e.getToken() !== 1074790415;) c.push(M$1(e, t, n, r, 4, {})); return D$1(e, i & 24 ? t | 32 : t, 1074790415), e.flags &= -4289, e.getToken() === 1077936155 && e.report(26), e.finishNode({ type: "BlockStatement", body: c }, s); } function an(e, t) { let { tokenStart: n } = e; switch (A$1(e, t), e.getToken()) { case 67108990: e.report(167); case 67174411: t & 512 || e.report(28), e.assignable = 2; break; case 69271571: case 67108877: t & 256 || e.report(29), e.assignable = 1; break; default: e.report(30, "super"); } return e.finishNode({ type: "Super" }, n); } function U(e, t, n, r, i, a) { let o = e.tokenStart; return W(e, t, n, G(e, t, n, 2, 0, r, i, a, o), i, 0, o); } function on(e, t, n, r) { e.assignable & 2 && e.report(55); let i = e.getToken(); return A$1(e, t), e.assignable = 2, e.finishNode({ type: "UpdateExpression", argument: n, operator: w$1[i & 255], prefix: !1 }, r); } function W(e, t, n, r, i, a, o) { if ((e.getToken() & 33619968) == 33619968 && !(e.flags & 1)) r = on(e, t, r, o); else if ((e.getToken() & 67108864) == 67108864) { switch (t = (t | 131072) ^ 131072, e.getToken()) { case 67108877: { A$1(e, (t | 262152) ^ 8), t & 16 && e.getToken() === 130 && e.tokenValue === "super" && e.report(173), e.assignable = 1; let i = cn(e, t | 64, n); r = e.finishNode({ type: "MemberExpression", object: r, computed: !1, property: i, optional: !1 }, o); break; } case 69271571: { let a = !1; (e.flags & 2048) == 2048 && (a = !0, e.flags = (e.flags | 2048) ^ 2048), A$1(e, t | 32); let { tokenStart: s } = e, c = z$1(e, t, n, i, 1, s); D$1(e, t, 20), e.assignable = 1, r = e.finishNode({ type: "MemberExpression", object: r, computed: !0, property: c, optional: !1 }, o), a && (e.flags |= 2048); break; } case 67174411: { if ((e.flags & 1024) == 1024) return e.flags = (e.flags | 1024) ^ 1024, r; let a = !1; (e.flags & 2048) == 2048 && (a = !0, e.flags = (e.flags | 2048) ^ 2048); let s = Cn(e, t, n, i); e.assignable = 2, r = e.finishNode({ type: "CallExpression", callee: r, arguments: s, optional: !1 }, o), a && (e.flags |= 2048); break; } case 67108990: A$1(e, (t | 262152) ^ 8), e.flags |= 2048, e.assignable = 2, r = sn(e, t, n, r, o); break; default: (e.flags & 2048) == 2048 && e.report(166), e.assignable = 2, r = e.finishNode({ type: "TaggedTemplateExpression", tag: r, quasi: e.getToken() === 67174408 ? bn(e, t | 64, n) : yn(e, t) }, o); } r = W(e, t, n, r, 0, 1, o); } return a === 0 && (e.flags & 2048) == 2048 && (e.flags = (e.flags | 2048) ^ 2048, r = e.finishNode({ type: "ChainExpression", expression: r }, o)), r; } function sn(e, t, n, r, i) { let a = !1, o; if ((e.getToken() === 69271571 || e.getToken() === 67174411) && (e.flags & 2048) == 2048 && (a = !0, e.flags = (e.flags | 2048) ^ 2048), e.getToken() === 69271571) { A$1(e, t | 32); let { tokenStart: a } = e, s = z$1(e, t, n, 0, 1, a); D$1(e, t, 20), e.assignable = 2, o = e.finishNode({ type: "MemberExpression", object: r, computed: !0, optional: !0, property: s }, i); } else if (e.getToken() === 67174411) { let a = Cn(e, t, n, 0); e.assignable = 2, o = e.finishNode({ type: "CallExpression", callee: r, arguments: a, optional: !0 }, i); } else { let a = cn(e, t, n); e.assignable = 2, o = e.finishNode({ type: "MemberExpression", object: r, computed: !1, optional: !0, property: a }, i); } return a && (e.flags |= 2048), o; } function cn(e, t, n) { return !(e.getToken() & 143360) && e.getToken() !== -2147483528 && e.getToken() !== -2147483527 && e.getToken() !== 130 && e.report(160), e.getToken() === 130 ? Yn(e, t, n, 0) : K(e, t); } function ln(e, t, n, r, i, a) { r && e.report(56), i || e.report(0); let o = e.getToken(); A$1(e, t | 32); let s = U(e, t, n, 0, 0, 1); return e.assignable & 2 && e.report(55), e.assignable = 2, e.finishNode({ type: "UpdateExpression", argument: s, operator: w$1[o & 255], prefix: !0 }, a); } function G(e, t, n, r, i, a, o, s, c) { if ((e.getToken() & 143360) == 143360) { switch (e.getToken()) { case 209006: return nn(e, t, n, i, o, c); case 241771: return tn(e, t, n, o, a, c); case 209005: return en(e, t, n, o, s, a, i, c); } let { tokenValue: l } = e, u = e.getToken(), d = K(e, t | 64); return e.getToken() === 10 ? (s || e.report(0), Ge(e, t, u), (u & 36864) == 36864 && (e.flags |= 256), Nn(e, t, n, l, d, i, a, 0, c)) : (t & 16 && !(t & 32768) && !(t & 8192) && e.tokenValue === "arguments" && e.report(130), (u & 255) == 73 && (t & 1 && e.report(113), r & 24 && e.report(100)), e.assignable = t & 1 && (u & 537079808) == 537079808 ? 2 : 1, d); } if ((e.getToken() & 134217728) == 134217728) return q(e, t); switch (e.getToken()) { case 33619993: case 33619994: return ln(e, t, n, i, s, c); case 16863276: case 16842798: case 16842799: case 25233968: case 25233969: case 16863275: case 16863277: return $t(e, t, n, s, o); case 86104: return En(e, t, n, 0, o, c); case 2162700: return kn(e, t, n, +!a, o); case 69271571: return Dn(e, t, n, +!a, o); case 67174411: return jn(e, t | 64, n, a, 1, 0, c); case 86021: case 86022: case 86023: return wn(e, t); case 86111: return Tn(e, t); case 65540: return Hn(e, t); case 132: case 86094: return Wn(e, t, n, o, c); case 86109: return an(e, t); case 67174409: return yn(e, t); case 67174408: return bn(e, t, n); case 86107: return Rn(e, t, n, o); case 134283388: return vn(e, t); case 130: return Yn(e, t, n, 0); case 86106: return un(e, t, n, i, o, c); case 8456256: if (e.options.jsx) return $n(e, t, n, 0, e.tokenStart); default: if (k$1(t, e.getToken())) return Mn(e, t, n); e.report(30, w$1[e.getToken() & 255]); } } function un(e, t, n, r, i, a) { let o = K(e, t); return e.getToken() === 67108877 ? dn(e, t, o, a) : (r && e.report(142), o = fn(e, t, n, i, a), e.assignable = 2, W(e, t, n, o, i, 0, a)); } function dn(e, t, n, r) { t & 2 || e.report(169), A$1(e, t); let i = e.getToken(); return i !== 209030 && e.tokenValue !== "meta" ? e.report(174) : i & -2147483648 && e.report(175), e.assignable = 2, e.finishNode({ type: "MetaProperty", meta: n, property: K(e, t) }, r); } function fn(e, t, n, r, i) { D$1(e, t | 32, 67174411), e.getToken() === 14 && e.report(143); let a = L$1(e, t, n, 1, r, e.tokenStart), o = null; e.getToken() === 18 && (D$1(e, t, 18), e.getToken() !== 16 && (o = L$1(e, (t | 131072) ^ 131072, n, 1, r, e.tokenStart)), E$1(e, t, 18)); let s = { type: "ImportExpression", source: a, options: o }; return D$1(e, t, 16), e.finishNode(s, i); } function pn(e, t) { if (!E$1(e, t, 20579)) return []; D$1(e, t, 2162700); let n = [], r = /* @__PURE__ */ new Set(); for (; e.getToken() !== 1074790415;) { let i = e.tokenStart, a = hn(e, t); D$1(e, t, 21); let o = mn(e, t), s = a.type === "Literal" ? a.value : a.name; r.has(s) && e.report(145, `${s}`), r.add(s), n.push(e.finishNode({ type: "ImportAttribute", key: a, value: o }, i)), e.getToken() !== 1074790415 && D$1(e, t, 18); } return D$1(e, t, 1074790415), n; } function mn(e, t) { if (e.getToken() === 134283267) return q(e, t); e.report(30, w$1[e.getToken() & 255]); } function hn(e, t) { if (e.getToken() === 134283267) return q(e, t); if (e.getToken() & 143360) return K(e, t); e.report(30, w$1[e.getToken() & 255]); } function gn(e, t) { let n = t.length; for (let r = 0; r < n; r++) { let i = t.charCodeAt(r); (i & 64512) == 55296 && (i > 56319 || ++r >= n || (t.charCodeAt(r) & 64512) != 56320) && e.report(171, JSON.stringify(t.charAt(r--))); } } function _n(e, t) { if (e.getToken() === 134283267) return gn(e, e.tokenValue), q(e, t); if (e.getToken() & 143360) return K(e, t); e.report(30, w$1[e.getToken() & 255]); } function vn(e, t) { let { tokenRaw: n, tokenValue: r, tokenStart: i } = e; A$1(e, t), e.assignable = 2; let a = { type: "Literal", value: r, bigint: String(r) }; return e.options.raw && (a.raw = n), e.finishNode(a, i); } function yn(e, t) { e.assignable = 2; let { tokenValue: n, tokenRaw: r, tokenStart: i } = e; D$1(e, t, 67174409); let a = [xn(e, n, r, i, !0)]; return e.finishNode({ type: "TemplateLiteral", expressions: [], quasis: a }, i); } function bn(e, t, n) { t = (t | 131072) ^ 131072; let { tokenValue: r, tokenRaw: i, tokenStart: a } = e; D$1(e, t & -65 | 32, 67174408); let o = [xn(e, r, i, a, !1)], s = [z$1(e, t & -65, n, 0, 1, e.tokenStart)]; for (e.getToken() !== 1074790415 && e.report(83); e.setToken(je(e, t), !0) !== 67174409;) { let { tokenValue: r, tokenRaw: i, tokenStart: a } = e; D$1(e, t & -65 | 32, 67174408), o.push(xn(e, r, i, a, !1)), s.push(z$1(e, t, n, 0, 1, e.tokenStart)), e.getToken() !== 1074790415 && e.report(83); } { let { tokenValue: n, tokenRaw: r, tokenStart: i } = e; D$1(e, t, 67174409), o.push(xn(e, n, r, i, !0)); } return e.finishNode({ type: "TemplateLiteral", expressions: s, quasis: o }, a); } function xn(e, t, n, r, i) { let a = e.finishNode({ type: "TemplateElement", value: { cooked: t, raw: n }, tail: i }, r), o = i ? 1 : 2; return e.options.ranges && (a.start += 1, a.range[0] += 1, a.end -= o, a.range[1] -= o), e.options.loc && (a.loc.start.column += 1, a.loc.end.column -= o), a; } function Sn(e, t, n) { let r = e.tokenStart; t = (t | 131072) ^ 131072, D$1(e, t | 32, 14); let i = L$1(e, t, n, 1, 0, e.tokenStart); return e.assignable = 1, e.finishNode({ type: "SpreadElement", argument: i }, r); } function Cn(e, t, n, r) { A$1(e, t | 32); let i = []; if (e.getToken() === 16) return A$1(e, t | 64), i; for (; e.getToken() !== 16 && (e.getToken() === 14 ? i.push(Sn(e, t, n)) : i.push(L$1(e, t, n, 1, r, e.tokenStart)), !(e.getToken() !== 18 || (A$1(e, t | 32), e.getToken() === 16)));); return D$1(e, t | 64, 16), i; } function K(e, t) { let { tokenValue: n, tokenStart: r } = e; return A$1(e, t | (n === "await" && !(e.getToken() & -2147483648) ? 32 : 0)), e.finishNode({ type: "Identifier", name: n }, r); } function q(e, t) { let { tokenValue: n, tokenRaw: r, tokenStart: i } = e; return e.getToken() === 134283388 ? vn(e, t) : (A$1(e, t), e.assignable = 2, e.finishNode(e.options.raw ? { type: "Literal", value: n, raw: r } : { type: "Literal", value: n }, i)); } function wn(e, t) { let n = e.tokenStart, r = w$1[e.getToken() & 255], i = e.getToken() === 86023 ? null : r === "true"; return A$1(e, t), e.assignable = 2, e.finishNode(e.options.raw ? { type: "Literal", value: i, raw: r } : { type: "Literal", value: i }, n); } function Tn(e, t) { let { tokenStart: n } = e; return A$1(e, t), e.assignable = 2, e.finishNode({ type: "ThisExpression" }, n); } function J(e, t, n, r, i, a, o, s, c) { A$1(e, t | 32); let l = a ? Le(e, t, 8391476) : 0, u = null, d, f = n ? e.createScope() : void 0; if (e.getToken() === 67174411) o & 1 || e.report(39, "Function"); else { let r = i & 4 && (!(t & 8) || !(t & 2)) ? 4 : 64 | (s ? 1024 : 0) | (l ? 1024 : 0); ze(e, t, e.getToken()), n && (r & 4 ? n.addVarName(t, e.tokenValue, r) : n.addBlockName(t, e.tokenValue, r, i), f = f?.createChildScope(128), o && o & 2 && e.declareUnboundVariable(e.tokenValue)), d = e.getToken(), e.getToken() & 143360 ? u = K(e, t) : e.report(30, w$1[e.getToken() & 255]); } { let e = 28416; t = (t | e) ^ e | 65536 | (s ? 2048 : 0) | (l ? 1024 : 0) | (l ? 0 : 262144); } f = f?.createChildScope(256); let p = In(e, (t | 8192) & -524289, f, r, 0, 1), m = 524428, h = rn(e, (t | m) ^ m | 36864, f?.createChildScope(64), r, 8, d, f); return e.finishNode({ type: "FunctionDeclaration", id: u, params: p, body: h, async: s === 1, generator: l === 1 }, c); } function En(e, t, n, r, i, a) { A$1(e, t | 32); let o = Le(e, t, 8391476), s = (r ? 2048 : 0) | (o ? 1024 : 0), c = null, l, u = e.createScopeIfLexical(), d = 552704; e.getToken() & 143360 && (ze(e, (t | d) ^ d | s, e.getToken()), u = u?.createChildScope(128), l = e.getToken(), c = K(e, t)), t = (t | d) ^ d | 65536 | s | (o ? 0 : 262144), u = u?.createChildScope(256); let f = In(e, (t | 8192) & -524289, u, n, i, 1), p = rn(e, t & -131229 | 36864, u?.createChildScope(64), n, 0, l, u); return e.assignable = 2, e.finishNode({ type: "FunctionExpression", id: c, params: f, body: p, async: r === 1, generator: o === 1 }, a); } function Dn(e, t, n, r, i) { let a = Y(e, t, void 0, n, r, i, 0, 2, 0); return e.destructible & 64 && e.report(63), e.destructible & 8 && e.report(62), a; } function Y(e, t, n, r, i, a, o, s, c) { let { tokenStart: l } = e; A$1(e, t | 32); let u = [], d = 0; for (t = (t | 131072) ^ 131072; e.getToken() !== 20;) if (E$1(e, t | 32, 18)) u.push(null); else { let i, { tokenStart: l, tokenValue: f } = e, p = e.getToken(); if (p & 143360) if (i = G(e, t, r, s, 0, 1, a, 1, l), e.getToken() === 1077936155) { e.assignable & 2 && e.report(26), A$1(e, t | 32), n?.addVarOrBlock(t, f, s, c); let u = L$1(e, t, r, 1, a, e.tokenStart); i = e.finishNode(o ? { type: "AssignmentPattern", left: i, right: u } : { type: "AssignmentExpression", operator: "=", left: i, right: u }, l), d |= e.destructible & 256 ? 256 : 0 | e.destructible & 128 ? 128 : 0; } else e.getToken() === 18 || e.getToken() === 20 ? (e.assignable & 2 ? d |= 16 : n?.addVarOrBlock(t, f, s, c), d |= e.destructible & 256 ? 256 : 0 | e.destructible & 128 ? 128 : 0) : (d |= s & 1 ? 32 : s & 2 ? 0 : 16, i = W(e, t, r, i, a, 0, l), e.getToken() !== 18 && e.getToken() !== 20 ? (e.getToken() !== 1077936155 && (d |= 16), i = B$1(e, t, r, a, o, l, i)) : e.getToken() !== 1077936155 && (d |= e.assignable & 2 ? 16 : 32)); else p & 2097152 ? (i = e.getToken() === 2162700 ? Q(e, t, n, r, 0, a, o, s, c) : Y(e, t, n, r, 0, a, o, s, c), d |= e.destructible, e.assignable = e.destructible & 16 ? 2 : 1, e.getToken() === 18 || e.getToken() === 20 ? e.assignable & 2 && (d |= 16) : e.destructible & 8 ? e.report(71) : (i = W(e, t, r, i, a, 0, l), d = e.assignable & 2 ? 16 : 0, e.getToken() !== 18 && e.getToken() !== 20 ? i = B$1(e, t, r, a, o, l, i) : e.getToken() !== 1077936155 && (d |= e.assignable & 2 ? 16 : 32))) : p === 14 ? (i = X(e, t, n, r, 20, s, c, 0, a, o), d |= e.destructible, e.getToken() !== 18 && e.getToken() !== 20 && e.report(30, w$1[e.getToken() & 255])) : (i = U(e, t, r, 1, 0, 1), e.getToken() !== 18 && e.getToken() !== 20 ? (i = B$1(e, t, r, a, o, l, i), !(s & 3) && p === 67174411 && (d |= 16)) : e.assignable & 2 ? d |= 16 : p === 67174411 && (d |= e.assignable & 1 && s & 3 ? 32 : 16)); if (u.push(i), E$1(e, t | 32, 18)) { if (e.getToken() === 20) break; } else break; } D$1(e, t, 20); let f = e.finishNode({ type: o ? "ArrayPattern" : "ArrayExpression", elements: u }, l); return !i && e.getToken() & 4194304 ? On(e, t, r, d, a, o, l, f) : (e.destructible = d, f); } function On(e, t, n, r, i, a, o, s) { e.getToken() !== 1077936155 && e.report(26), A$1(e, t | 32), r & 16 && e.report(26), a || O(e, s); let { tokenStart: c } = e, l = L$1(e, t, n, 1, i, c); return e.destructible = (r | 72) ^ 72 | (e.destructible & 128 ? 128 : 0) | (e.destructible & 256 ? 256 : 0), e.finishNode(a ? { type: "AssignmentPattern", left: s, right: l } : { type: "AssignmentExpression", left: s, operator: "=", right: l }, o); } function X(e, t, n, r, i, a, o, s, c, l) { let { tokenStart: u } = e; A$1(e, t | 32); let d = null, f = 0, { tokenValue: p, tokenStart: m } = e, h = e.getToken(); if (h & 143360) e.assignable = 1, d = G(e, t, r, a, 0, 1, c, 1, m), h = e.getToken(), d = W(e, t, r, d, c, 0, m), e.getToken() !== 18 && e.getToken() !== i && (e.assignable & 2 && e.getToken() === 1077936155 && e.report(71), f |= 16, d = B$1(e, t, r, c, l, m, d)), e.assignable & 2 ? f |= 16 : h === i || h === 18 ? n?.addVarOrBlock(t, p, a, o) : f |= 32, f |= e.destructible & 128 ? 128 : 0; else if (h === i) e.report(41); else if (h & 2097152) d = e.getToken() === 2162700 ? Q(e, t, n, r, 1, c, l, a, o) : Y(e, t, n, r, 1, c, l, a, o), h = e.getToken(), h !== 1077936155 && h !== i && h !== 18 ? (e.destructible & 8 && e.report(71), d = W(e, t, r, d, c, 0, m), f |= e.assignable & 2 ? 16 : 0, (e.getToken() & 4194304) == 4194304 ? (e.getToken() !== 1077936155 && (f |= 16), d = B$1(e, t, r, c, l, m, d)) : ((e.getToken() & 8388608) == 8388608 && (d = H$1(e, t, r, 1, m, 4, h, d)), E$1(e, t | 32, 22) && (d = V$1(e, t, r, d, m)), f |= e.assignable & 2 ? 16 : 32)) : f |= i === 1074790415 && h !== 1077936155 ? 16 : e.destructible; else { f |= 32, d = U(e, t, r, 1, c, 1); let { tokenStart: n } = e, a = e.getToken(); return a === 1077936155 ? (e.assignable & 2 && e.report(26), d = B$1(e, t, r, c, l, n, d), f |= 16) : (a === 18 ? f |= 16 : a !== i && (d = B$1(e, t, r, c, l, n, d)), f |= e.assignable & 1 ? 32 : 16), e.destructible = f, e.getToken() !== i && e.getToken() !== 18 && e.report(161), e.finishNode({ type: l ? "RestElement" : "SpreadElement", argument: d }, u); } if (e.getToken() !== i) if (a & 1 && (f |= s ? 16 : 32), E$1(e, t | 32, 1077936155)) { f & 16 && e.report(26), O(e, d); let n = L$1(e, t, r, 1, c, e.tokenStart); d = e.finishNode(l ? { type: "AssignmentPattern", left: d, right: n } : { type: "AssignmentExpression", left: d, operator: "=", right: n }, m), f = 16; } else f |= 16; return e.destructible = f, e.finishNode({ type: l ? "RestElement" : "SpreadElement", argument: d }, u); } function Z$1(e, t, n, r, i, a) { let o = 11264 | (r & 64 ? 0 : 16896); t = (t | o) ^ o | (r & 8 ? 1024 : 0) | (r & 16 ? 2048 : 0) | (r & 64 ? 16384 : 0) | 98560; let s = e.createScopeIfLexical(256), c = An(e, (t | 8192) & -524289, s, n, r, 1, i); s = s?.createChildScope(64); let l = rn(e, t & -655373 | 36864, s, n, 0, void 0, s?.parent); return e.finishNode({ type: "FunctionExpression", params: c, body: l, async: (r & 16) > 0, generator: (r & 8) > 0, id: null }, a); } function kn(e, t, n, r, i) { let a = Q(e, t, void 0, n, r, i, 0, 2, 0); return e.destructible & 64 && e.report(63), e.destructible & 8 && e.report(62), a; } function Q(e, t, n, r, i, a, o, s, c) { let { tokenStart: l } = e; A$1(e, t); let u = [], d = 0, f = 0; for (t = (t | 131072) ^ 131072; e.getToken() !== 1074790415;) { let { tokenValue: i, tokenStart: l } = e, p = e.getToken(); if (p === 14) u.push(X(e, t, n, r, 1074790415, s, c, 0, a, o)); else { let m = 0, h = null, g; if (e.getToken() & 143360 || e.getToken() === -2147483528 || e.getToken() === -2147483527) if (e.getToken() === -2147483527 && (d |= 16), h = K(e, t), e.getToken() === 18 || e.getToken() === 1074790415 || e.getToken() === 1077936155) if (m |= 4, t & 1 && (p & 537079808) == 537079808 ? d |= 16 : Re(e, t, s, p, 0), n?.addVarOrBlock(t, i, s, c), E$1(e, t | 32, 1077936155)) { d |= 8; let n = L$1(e, t, r, 1, a, e.tokenStart); d |= e.destructible & 256 ? 256 : 0 | e.destructible & 128 ? 128 : 0, g = e.finishNode({ type: "AssignmentPattern", left: e.options.uniqueKeyInPattern ? Object.assign({}, h) : h, right: n }, l); } else d |= (p === 209006 ? 128 : 0) | (p === -2147483528 ? 16 : 0), g = e.options.uniqueKeyInPattern ? Object.assign({}, h) : h; else if (E$1(e, t | 32, 21)) { let { tokenStart: l } = e; if (i === "__proto__" && f++, e.getToken() & 143360) { let i = e.getToken(), u = e.tokenValue; g = G(e, t, r, s, 0, 1, a, 1, l); let f = e.getToken(); g = W(e, t, r, g, a, 0, l), e.getToken() === 18 || e.getToken() === 1074790415 ? f === 1077936155 || f === 1074790415 || f === 18 ? (d |= e.destructible & 128 ? 128 : 0, e.assignable & 2 ? d |= 16 : (i & 143360) == 143360 && n?.addVarOrBlock(t, u, s, c)) : d |= e.assignable & 1 ? 32 : 16 : (e.getToken() & 4194304) == 4194304 ? (e.assignable & 2 ? d |= 16 : f === 1077936155 ? n?.addVarOrBlock(t, u, s, c) : d |= 32, g = B$1(e, t, r, a, o, l, g)) : (d |= 16, (e.getToken() & 8388608) == 8388608 && (g = H$1(e, t, r, 1, l, 4, f, g)), E$1(e, t | 32, 22) && (g = V$1(e, t, r, g, l))); } else (e.getToken() & 2097152) == 2097152 ? (g = e.getToken() === 69271571 ? Y(e, t, n, r, 0, a, o, s, c) : Q(e, t, n, r, 0, a, o, s, c), d = e.destructible, e.assignable = d & 16 ? 2 : 1, e.getToken() === 18 || e.getToken() === 1074790415 ? e.assignable & 2 && (d |= 16) : e.destructible & 8 ? e.report(71) : (g = W(e, t, r, g, a, 0, l), d = e.assignable & 2 ? 16 : 0, (e.getToken() & 4194304) == 4194304 ? g = Qt(e, t, r, a, o, l, g) : ((e.getToken() & 8388608) == 8388608 && (g = H$1(e, t, r, 1, l, 4, p, g)), E$1(e, t | 32, 22) && (g = V$1(e, t, r, g, l)), d |= e.assignable & 2 ? 16 : 32))) : (g = U(e, t, r, 1, a, 1), d |= e.assignable & 1 ? 32 : 16, e.getToken() === 18 || e.getToken() === 1074790415 ? e.assignable & 2 && (d |= 16) : (g = W(e, t, r, g, a, 0, l), d = e.assignable & 2 ? 16 : 0, e.getToken() !== 18 && p !== 1074790415 && (e.getToken() !== 1077936155 && (d |= 16), g = B$1(e, t, r, a, o, l, g)))); } else e.getToken() === 69271571 ? (d |= 16, p === 209005 && (m |= 16), m |= (p === 209008 ? 256 : p === 209009 ? 512 : 1) | 2, h = $$1(e, t, r, a), d |= e.assignable, g = Z$1(e, t, r, m, a, e.tokenStart)) : e.getToken() & 143360 ? (d |= 16, p === -2147483528 && e.report(95), p === 209005 ? (e.flags & 1 && e.report(132), m |= 17) : p === 209008 ? m |= 256 : p === 209009 ? m |= 512 : e.report(0), h = K(e, t), g = Z$1(e, t, r, m, a, e.tokenStart)) : e.getToken() === 67174411 ? (d |= 16, m |= 1, g = Z$1(e, t, r, m, a, e.tokenStart)) : e.getToken() === 8391476 ? (d |= 16, p === 209008 ? e.report(42) : p === 209009 ? e.report(43) : p !== 209005 && e.report(30, w$1[52]), A$1(e, t), m |= 9 | (p === 209005 ? 16 : 0), e.getToken() & 143360 ? h = K(e, t) : (e.getToken() & 134217728) == 134217728 ? h = q(e, t) : e.getToken() === 69271571 ? (m |= 2, h = $$1(e, t, r, a), d |= e.assignable) : e.report(30, w$1[e.getToken() & 255]), g = Z$1(e, t, r, m, a, e.tokenStart)) : (e.getToken() & 134217728) == 134217728 ? (p === 209005 && (m |= 16), m |= p === 209008 ? 256 : p === 209009 ? 512 : 1, d |= 16, h = q(e, t), g = Z$1(e, t, r, m, a, e.tokenStart)) : e.report(133); else if ((e.getToken() & 134217728) == 134217728) if (h = q(e, t), e.getToken() === 21) { D$1(e, t | 32, 21); let { tokenStart: l } = e; if (i === "__proto__" && f++, e.getToken() & 143360) { g = G(e, t, r, s, 0, 1, a, 1, l); let { tokenValue: i } = e, u = e.getToken(); g = W(e, t, r, g, a, 0, l), e.getToken() === 18 || e.getToken() === 1074790415 ? u === 1077936155 || u === 1074790415 || u === 18 ? e.assignable & 2 ? d |= 16 : n?.addVarOrBlock(t, i, s, c) : d |= e.assignable & 1 ? 32 : 16 : e.getToken() === 1077936155 ? (e.assignable & 2 && (d |= 16), g = B$1(e, t, r, a, o, l, g)) : (d |= 16, g = B$1(e, t, r, a, o, l, g)); } else (e.getToken() & 2097152) == 2097152 ? (g = e.getToken() === 69271571 ? Y(e, t, n, r, 0, a, o, s, c) : Q(e, t, n, r, 0, a, o, s, c), d = e.destructible, e.assignable = d & 16 ? 2 : 1, e.getToken() === 18 || e.getToken() === 1074790415 ? e.assignable & 2 && (d |= 16) : (e.destructible & 8) != 8 && (g = W(e, t, r, g, a, 0, l), d = e.assignable & 2 ? 16 : 0, (e.getToken() & 4194304) == 4194304 ? g = Qt(e, t, r, a, o, l, g) : ((e.getToken() & 8388608) == 8388608 && (g = H$1(e, t, r, 1, l, 4, p, g)), E$1(e, t | 32, 22) && (g = V$1(e, t, r, g, l)), d |= e.assignable & 2 ? 16 : 32))) : (g = U(e, t, r, 1, 0, 1), d |= e.assignable & 1 ? 32 : 16, e.getToken() === 18 || e.getToken() === 1074790415 ? e.assignable & 2 && (d |= 16) : (g = W(e, t, r, g, a, 0, l), d = e.assignable & 1 ? 0 : 16, e.getToken() !== 18 && e.getToken() !== 1074790415 && (e.getToken() !== 1077936155 && (d |= 16), g = B$1(e, t, r, a, o, l, g)))); } else e.getToken() === 67174411 ? (m |= 1, g = Z$1(e, t, r, m, a, e.tokenStart), d = e.assignable | 16) : e.report(134); else if (e.getToken() === 69271571) if (h = $$1(e, t, r, a), d |= e.destructible & 256 ? 256 : 0, m |= 2, e.getToken() === 21) { A$1(e, t | 32); let { tokenStart: i, tokenValue: l } = e, u = e.getToken(); if (e.getToken() & 143360) { g = G(e, t, r, s, 0, 1, a, 1, i); let f = e.getToken(); g = W(e, t, r, g, a, 0, i), (e.getToken() & 4194304) == 4194304 ? (d |= e.assignable & 2 ? 16 : f === 1077936155 ? 0 : 32, g = Qt(e, t, r, a, o, i, g)) : e.getToken() === 18 || e.getToken() === 1074790415 ? f === 1077936155 || f === 1074790415 || f === 18 ? e.assignable & 2 ? d |= 16 : (u & 143360) == 143360 && n?.addVarOrBlock(t, l, s, c) : d |= e.assignable & 1 ? 32 : 16 : (d |= 16, g = B$1(e, t, r, a, o, i, g)); } else (e.getToken() & 2097152) == 2097152 ? (g = e.getToken() === 69271571 ? Y(e, t, n, r, 0, a, o, s, c) : Q(e, t, n, r, 0, a, o, s, c), d = e.destructible, e.assignable = d & 16 ? 2 : 1, e.getToken() === 18 || e.getToken() === 1074790415 ? e.assignable & 2 && (d |= 16) : d & 8 ? e.report(62) : (g = W(e, t, r, g, a, 0, i), d = e.assignable & 2 ? d | 16 : 0, (e.getToken() & 4194304) == 4194304 ? (e.getToken() !== 1077936155 && (d |= 16), g = Qt(e, t, r, a, o, i, g)) : ((e.getToken() & 8388608) == 8388608 && (g = H$1(e, t, r, 1, i, 4, p, g)), E$1(e, t | 32, 22) && (g = V$1(e, t, r, g, i)), d |= e.assignable & 2 ? 16 : 32))) : (g = U(e, t, r, 1, 0, 1), d |= e.assignable & 1 ? 32 : 16, e.getToken() === 18 || e.getToken() === 1074790415 ? e.assignable & 2 && (d |= 16) : (g = W(e, t, r, g, a, 0, i), d = e.assignable & 1 ? 0 : 16, e.getToken() !== 18 && e.getToken() !== 1074790415 && (e.getToken() !== 1077936155 && (d |= 16), g = B$1(e, t, r, a, o, i, g)))); } else e.getToken() === 67174411 ? (m |= 1, g = Z$1(e, t, r, m, a, e.tokenStart), d = 16) : e.report(44); else if (p === 8391476) if (D$1(e, t | 32, 8391476), m |= 8, e.getToken() & 143360) { let n = e.getToken(); if (h = K(e, t), m |= 1, e.getToken() === 67174411) d |= 16, g = Z$1(e, t, r, m, a, e.tokenStart); else throw new C$1(e.tokenStart, e.currentLocation, n === 209005 ? 46 : n === 209008 || e.getToken() === 209009 ? 45 : 47, w$1[n & 255]); } else (e.getToken() & 134217728) == 134217728 ? (d |= 16, h = q(e, t), m |= 1, g = Z$1(e, t, r, m, a, e.tokenStart)) : e.getToken() === 69271571 ? (d |= 16, m |= 3, h = $$1(e, t, r, a), g = Z$1(e, t, r, m, a, e.tokenStart)) : e.report(126); else e.report(30, w$1[p & 255]); d |= e.destructible & 128 ? 128 : 0, e.destructible = d, u.push(e.finishNode({ type: "Property", key: h, value: g, kind: m & 768 ? m & 512 ? "set" : "get" : "init", computed: (m & 2) > 0, method: (m & 1) > 0, shorthand: (m & 4) > 0 }, l)); } if (d |= e.destructible, e.getToken() !== 18) break; A$1(e, t); } D$1(e, t, 1074790415), f > 1 && (d |= 64); let p = e.finishNode({ type: o ? "ObjectPattern" : "ObjectExpression", properties: u }, l); return !i && e.getToken() & 4194304 ? On(e, t, r, d, a, o, l, p) : (e.destructible = d, p); } function An(e, t, n, r, i, a, o) { D$1(e, t, 67174411); let s = []; if (e.flags = (e.flags | 128) ^ 128, e.getToken() === 16) return i & 512 && e.report(37, "Setter", "one", ""), A$1(e, t), s; i & 256 && e.report(37, "Getter", "no", "s"), i & 512 && e.getToken() === 14 && e.report(38), t = (t | 131072) ^ 131072; let c = 0, l = 0; for (; e.getToken() !== 18;) { let u = null, { tokenStart: d } = e; if (e.getToken() & 143360 ? (t & 1 || ((e.getToken() & 36864) == 36864 && (e.flags |= 256), (e.getToken() & 537079808) == 537079808 && (e.flags |= 512)), u = Qn(e, t, n, i | 1, 0)) : (e.getToken() === 2162700 ? u = Q(e, t, n, r, 1, o, 1, a, 0) : e.getToken() === 69271571 ? u = Y(e, t, n, r, 1, o, 1, a, 0) : e.getToken() === 14 && (u = X(e, t, n, r, 16, a, 0, 0, o, 1)), l = 1, e.destructible & 48 && e.report(50)), e.getToken() === 1077936155) { A$1(e, t | 32), l = 1; let n = L$1(e, t, r, 1, 0, e.tokenStart); u = e.finishNode({ type: "AssignmentPattern", left: u, right: n }, d); } if (c++, s.push(u), !E$1(e, t, 18) || e.getToken() === 16) break; } return i & 512 && c !== 1 && e.report(37, "Setter", "one", ""), n?.reportScopeError(), l && (e.flags |= 128), D$1(e, t, 16), s; } function $$1(e, t, n, r) { A$1(e, t | 32); let i = L$1(e, (t | 131072) ^ 131072, n, 1, r, e.tokenStart); return D$1(e, t, 20), i; } function jn(e, t, n, r, i, a, o) { e.flags = (e.flags | 128) ^ 128; let s = e.tokenStart; A$1(e, t | 262176); let c = e.createScopeIfLexical()?.createChildScope(512); if (t = (t | 131072) ^ 131072, E$1(e, t, 16)) return Pn(e, t, c, n, [], r, 0, o); let l = 0; e.destructible &= -385; let u, d = [], f = 0, p = 0, m = 0, h = e.tokenStart; for (e.assignable = 1; e.getToken() !== 16;) { let { tokenStart: r } = e, o = e.getToken(); if (o & 143360) c?.addBlockName(t, e.tokenValue, 1, 0), (o & 537079808) == 537079808 ? p = 1 : (o & 36864) == 36864 && (m = 1), u = G(e, t, n, i, 0, 1, 1, 1, r), e.getToken() === 16 || e.getToken() === 18 ? e.assignable & 2 && (l |= 16, p = 1) : (e.getToken() === 1077936155 ? p = 1 : l |= 16, u = W(e, t, n, u, 1, 0, r), e.getToken() !== 16 && e.getToken() !== 18 && (u = B$1(e, t, n, 1, 0, r, u))); else if ((o & 2097152) == 2097152) u = o === 2162700 ? Q(e, t | 262144, c, n, 0, 1, 0, i, a) : Y(e, t | 262144, c, n, 0, 1, 0, i, a), l |= e.destructible, p = 1, e.assignable = 2, e.getToken() !== 16 && e.getToken() !== 18 && (l & 8 && e.report(122), u = W(e, t, n, u, 0, 0, r), l |= 16, e.getToken() !== 16 && e.getToken() !== 18 && (u = B$1(e, t, n, 0, 0, r, u))); else if (o === 14) { u = X(e, t, c, n, 16, i, a, 0, 1, 0), e.destructible & 16 && e.report(74), p = 1, f && (e.getToken() === 16 || e.getToken() === 18) && d.push(u), l |= 8; break; } else { if (l |= 16, u = L$1(e, t, n, 1, 1, r), f && (e.getToken() === 16 || e.getToken() === 18) && d.push(u), e.getToken() === 18 && (f || (f = 1, d = [u])), f) { for (; E$1(e, t | 32, 18);) d.push(L$1(e, t, n, 1, 1, e.tokenStart)); e.assignable = 2, u = e.finishNode({ type: "SequenceExpression", expressions: d }, h); } return D$1(e, t, 16), e.destructible = l, e.options.preserveParens ? e.finishNode({ type: "ParenthesizedExpression", expression: u }, s) : u; } if (f && (e.getToken() === 16 || e.getToken() === 18) && d.push(u), !E$1(e, t | 32, 18)) break; if (f || (f = 1, d = [u]), e.getToken() === 16) { l |= 8; break; } } return f && (e.assignable = 2, u = e.finishNode({ type: "SequenceExpression", expressions: d }, h)), D$1(e, t, 16), l & 16 && l & 8 && e.report(151), l |= e.destructible & 256 ? 256 : 0 | e.destructible & 128 ? 128 : 0, e.getToken() === 10 ? (l & 48 && e.report(49), t & 2050 && l & 128 && e.report(31), t & 1025 && l & 256 && e.report(32), p && (e.flags |= 128), m && (e.flags |= 256), Pn(e, t, c, n, f ? d : [u], r, 0, o)) : (l & 64 && e.report(63), l & 8 && e.report(144), e.destructible = (e.destructible | 256) ^ 256 | l, e.options.preserveParens ? e.finishNode({ type: "ParenthesizedExpression", expression: u }, s) : u); } function Mn(e, t, n) { let { tokenStart: r } = e, { tokenValue: i } = e, a = 0, o = 0; (e.getToken() & 537079808) == 537079808 ? a = 1 : (e.getToken() & 36864) == 36864 && (o = 1); let s = K(e, t); if (e.assignable = 1, e.getToken() === 10) { let c = e.options.lexical ? lt(e, t, i) : void 0; return a && (e.flags |= 128), o && (e.flags |= 256), Fn(e, t, c, n, [s], 0, r); } return s; } function Nn(e, t, n, r, i, a, o, s, c) { return o || e.report(57), a && e.report(51), e.flags &= -129, Fn(e, t, e.options.lexical ? lt(e, t, r) : void 0, n, [i], s, c); } function Pn(e, t, n, r, i, a, o, s) { a || e.report(57); for (let t = 0; t < i.length; ++t) O(e, i[t]); return Fn(e, t, n, r, i, o, s); } function Fn(e, t, n, r, i, a, o) { e.flags & 1 && e.report(48), D$1(e, t | 32, 10); let s = 535552; t = (t | s) ^ s | (a ? 2048 : 0); let c = e.getToken() !== 2162700, l; if (n?.reportScopeError(), c) e.flags = (e.flags | 4928) ^ 4928, l = L$1(e, t, r, 1, 0, e.tokenStart); else { n = n?.createChildScope(64); let i = 131084; switch (l = rn(e, (t | i) ^ i | 4096, n, r, 16, void 0, void 0), e.getToken()) { case 69271571: e.flags & 1 || e.report(116); break; case 67108877: case 67174409: case 22: e.report(117); case 67174411: e.flags & 1 || e.report(116), e.flags |= 1024; } (e.getToken() & 8388608) == 8388608 && !(e.flags & 1) && e.report(30, w$1[e.getToken() & 255]), (e.getToken() & 33619968) == 33619968 && e.report(125); } return e.assignable = 2, e.finishNode({ type: "ArrowFunctionExpression", params: i, body: l, async: a === 1, expression: c, generator: !1 }, o); } function In(e, t, n, r, i, a) { D$1(e, t, 67174411), e.flags = (e.flags | 128) ^ 128; let o = []; if (E$1(e, t, 16)) return o; t = (t | 131072) ^ 131072; let s = 0; for (; e.getToken() !== 18;) { let c, { tokenStart: l } = e, u = e.getToken(); if (u & 143360 ? (t & 1 || ((u & 36864) == 36864 && (e.flags |= 256), (u & 537079808) == 537079808 && (e.flags |= 512)), c = Qn(e, t, n, a | 1, 0)) : (u === 2162700 ? c = Q(e, t, n, r, 1, i, 1, a, 0) : u === 69271571 ? c = Y(e, t, n, r, 1, i, 1, a, 0) : u === 14 ? c = X(e, t, n, r, 16, a, 0, 0, i, 1) : e.report(30, w$1[u & 255]), s = 1, e.destructible & 48 && e.report(50)), e.getToken() === 1077936155) { A$1(e, t | 32), s = 1; let n = L$1(e, t, r, 1, i, e.tokenStart); c = e.finishNode({ type: "AssignmentPattern", left: c, right: n }, l); } if (o.push(c), !E$1(e, t, 18) || e.getToken() === 16) break; } return s && (e.flags |= 128), (s || t & 1) && n?.reportScopeError(), D$1(e, t, 16), o; } function Ln(e, t, n, r, i, a) { let o = e.getToken(); if (o & 67108864) { if (o === 67108877) { A$1(e, t | 262144), e.assignable = 1; let i = cn(e, t, n); return Ln(e, t, n, e.finishNode({ type: "MemberExpression", object: r, computed: !1, property: i, optional: !1 }, a), 0, a); } if (o === 69271571) { A$1(e, t | 32); let { tokenStart: o } = e, s = z$1(e, t, n, i, 1, o); return D$1(e, t, 20), e.assignable = 1, Ln(e, t, n, e.finishNode({ type: "MemberExpression", object: r, computed: !0, property: s, optional: !1 }, a), 0, a); } if (o === 67174408 || o === 67174409) return e.assignable = 2, Ln(e, t, n, e.finishNode({ type: "TaggedTemplateExpression", tag: r, quasi: e.getToken() === 67174408 ? bn(e, t | 64, n) : yn(e, t | 64) }, a), 0, a); } return r; } function Rn(e, t, n, r) { let { tokenStart: i } = e, a = K(e, t | 32), { tokenStart: o } = e; if (E$1(e, t, 67108877)) { if (t & 65536 && e.getToken() === 209029) return e.assignable = 2, zn(e, t, a, i); e.report(94); } e.assignable = 2, (e.getToken() & 16842752) == 16842752 && e.report(65, w$1[e.getToken() & 255]); let s = G(e, t, n, 2, 1, 0, r, 1, o); t = (t | 131072) ^ 131072, e.getToken() === 67108990 && e.report(168); let c = Ln(e, t, n, s, r, o); return e.assignable = 2, e.finishNode({ type: "NewExpression", callee: c, arguments: e.getToken() === 67174411 ? Cn(e, t, n, r) : [] }, i); } function zn(e, t, n, r) { let i = K(e, t); return e.finishNode({ type: "MetaProperty", meta: n, property: i }, r); } function Bn(e, t, n, r, i) { return e.getToken() === 209006 && e.report(31), t & 1025 && e.getToken() === 241771 && e.report(32), Ge(e, t, e.getToken()), (e.getToken() & 36864) == 36864 && (e.flags |= 256), Nn(e, t & -524289 | 2048, n, e.tokenValue, K(e, t), 0, r, 1, i); } function Vn(e, t, n, r, i, a, o, s, c) { A$1(e, t | 32); let l = e.createScopeIfLexical()?.createChildScope(512); if (t = (t | 131072) ^ 131072, E$1(e, t, 16)) return e.getToken() === 10 ? (s & 1 && e.report(48), Pn(e, t, l, n, [], i, 1, c)) : e.finishNode({ type: "CallExpression", callee: r, arguments: [], optional: !1 }, c); let u = 0, d = null, f = 0; e.destructible = (e.destructible | 384) ^ 384; let p = []; for (; e.getToken() !== 16;) { let { tokenStart: i } = e, s = e.getToken(); if (s & 143360) l?.addBlockName(t, e.tokenValue, a, 0), (s & 537079808) == 537079808 ? e.flags |= 512 : (s & 36864) == 36864 && (e.flags |= 256), d = G(e, t, n, a, 0, 1, 1, 1, i), e.getToken() === 16 || e.getToken() === 18 ? e.assignable & 2 && (u |= 16, f = 1) : (e.getToken() === 1077936155 ? f = 1 : u |= 16, d = W(e, t, n, d, 1, 0, i), e.getToken() !== 16 && e.getToken() !== 18 && (d = B$1(e, t, n, 1, 0, i, d))); else if (s & 2097152) d = s === 2162700 ? Q(e, t, l, n, 0, 1, 0, a, o) : Y(e, t, l, n, 0, 1, 0, a, o), u |= e.destructible, f = 1, e.getToken() !== 16 && e.getToken() !== 18 && (u & 8 && e.report(122), d = W(e, t, n, d, 0, 0, i), u |= 16, (e.getToken() & 8388608) == 8388608 && (d = H$1(e, t, n, 1, c, 4, s, d)), E$1(e, t | 32, 22) && (d = V$1(e, t, n, d, c))); else if (s === 14) d = X(e, t, l, n, 16, a, o, 1, 1, 0), u |= (e.getToken() === 16 ? 0 : 16) | e.destructible, f = 1; else { for (d = L$1(e, t, n, 1, 0, i), u = e.assignable, p.push(d); E$1(e, t | 32, 18);) p.push(L$1(e, t, n, 1, 0, i)); return u |= e.assignable, D$1(e, t, 16), e.destructible = u | 16, e.assignable = 2, e.finishNode({ type: "CallExpression", callee: r, arguments: p, optional: !1 }, c); } if (p.push(d), !E$1(e, t | 32, 18)) break; } return D$1(e, t, 16), u |= e.destructible & 256 ? 256 : 0 | e.destructible & 128 ? 128 : 0, e.getToken() === 10 ? (u & 48 && e.report(27), (e.flags & 1 || s & 1) && e.report(48), u & 128 && e.report(31), t & 1025 && u & 256 && e.report(32), f && (e.flags |= 128), Pn(e, t | 2048, l, n, p, i, 1, c)) : (u & 64 && e.report(63), u & 8 && e.report(62), e.assignable = 2, e.finishNode({ type: "CallExpression", callee: r, arguments: p, optional: !1 }, c)); } function Hn(e, t) { let { tokenRaw: n, tokenRegExp: r, tokenValue: i, tokenStart: a } = e; A$1(e, t), e.assignable = 2; let o = { type: "Literal", value: i, regex: r }; return e.options.raw && (o.raw = n), e.finishNode(o, a); } function Un(e, t, n, r, i) { let a, o; e.leadingDecorators.decorators.length ? (e.getToken() === 132 && e.report(30, "@"), a = e.leadingDecorators.start, o = [...e.leadingDecorators.decorators], e.leadingDecorators.decorators.length = 0) : (a = e.tokenStart, o = Gn(e, t, r)), t = (t | 16385) ^ 16384, A$1(e, t); let s = null, c = null, { tokenValue: l } = e; e.getToken() & 4096 && e.getToken() !== 20565 ? (Be(e, t, e.getToken()) && e.report(118), (e.getToken() & 537079808) == 537079808 && e.report(119), n && (n.addBlockName(t, l, 32, 0), i && i & 2 && e.declareUnboundVariable(l)), s = K(e, t)) : i & 1 || e.report(39, "Class"); let u = t; E$1(e, t | 32, 20565) ? (c = U(e, t, r, 0, 0, 0), u |= 512) : u = (u | 512) ^ 512; let d = qn(e, u, t, n, r, 2, 8, 0); return e.finishNode({ type: "ClassDeclaration", id: s, superClass: c, body: d, ...e.options.next ? { decorators: o } : null }, a); } function Wn(e, t, n, r, i) { let a = null, o = null, s = Gn(e, t, n); t = (t | 16385) ^ 16384, A$1(e, t), e.getToken() & 4096 && e.getToken() !== 20565 && (Be(e, t, e.getToken()) && e.report(118), (e.getToken() & 537079808) == 537079808 && e.report(119), a = K(e, t)); let c = t; E$1(e, t | 32, 20565) ? (o = U(e, t, n, 0, r, 0), c |= 512) : c = (c | 512) ^ 512; let l = qn(e, c, t, void 0, n, 2, 0, r); return e.assignable = 2, e.finishNode({ type: "ClassExpression", id: a, superClass: o, body: l, ...e.options.next ? { decorators: s } : null }, i); } function Gn(e, t, n) { let r = []; if (e.options.next) for (; e.getToken() === 132;) r.push(Kn(e, t, n)); return r; } function Kn(e, t, n) { let r = e.tokenStart; A$1(e, t | 32); let i = G(e, t, n, 2, 0, 1, 0, 1, r); return i = W(e, t, n, i, 0, 0, e.tokenStart), e.finishNode({ type: "Decorator", expression: i }, r); } function qn(e, t, n, r, i, a, o, s) { let { tokenStart: c } = e, l = e.createPrivateScopeIfLexical(i); D$1(e, t | 32, 2162700); let u = 655360; t = (t | u) ^ u; let d = e.flags & 32; e.flags = (e.flags | 32) ^ 32; let f = []; for (; e.getToken() !== 1074790415;) { let i = e.tokenStart, o = Gn(e, t, l); if (o.length > 0 && e.tokenValue === "constructor" && e.report(109), e.getToken() === 1074790415 && e.report(108), E$1(e, t, 1074790417)) { o.length > 0 && e.report(120); continue; } f.push(Jn(e, t, r, l, n, a, o, 0, s, o.length > 0 ? i : e.tokenStart)); } return D$1(e, o & 8 ? t | 32 : t, 1074790415), l?.validatePrivateIdentifierRefs(), e.flags = e.flags & -33 | d, e.finishNode({ type: "ClassBody", body: f }, c); } function Jn(e, t, n, r, i, a, o, s, c, l) { let u = s ? 32 : 0, d = null, f = e.getToken(); if (f & 176128 || f === -2147483528) switch (d = K(e, t), f) { case 36970: if (!s && e.getToken() !== 67174411 && (e.getToken() & 1048576) != 1048576 && e.getToken() !== 1077936155) return Jn(e, t, n, r, i, a, o, 1, c, l); break; case 209005: if (e.getToken() !== 67174411 && !(e.flags & 1)) { if ((e.getToken() & 1073741824) == 1073741824) return Xn(e, t, r, d, u, o, l); u |= 16 | (Le(e, t, 8391476) ? 8 : 0); } break; case 209008: if (e.getToken() !== 67174411) { if ((e.getToken() & 1073741824) == 1073741824) return Xn(e, t, r, d, u, o, l); u |= 256; } break; case 209009: if (e.getToken() !== 67174411) { if ((e.getToken() & 1073741824) == 1073741824) return Xn(e, t, r, d, u, o, l); u |= 512; } break; case 12402: if (e.getToken() !== 67174411 && !(e.flags & 1)) { if ((e.getToken() & 1073741824) == 1073741824) return Xn(e, t, r, d, u, o, l); e.options.next && (u |= 1024); } } else if (f === 69271571) u |= 2, d = $$1(e, i, r, c); else if ((f & 134217728) == 134217728) d = q(e, t); else if (f === 8391476) u |= 8, A$1(e, t); else if (e.getToken() === 130) u |= 8192, d = Yn(e, t | 16, r, 768); else if ((e.getToken() & 1073741824) == 1073741824) u |= 128; else if (s && f === 2162700) return Lt(e, t | 16, n, r, l); else f === -2147483527 ? (d = K(e, t), e.getToken() !== 67174411 && e.report(30, w$1[e.getToken() & 255])) : e.report(30, w$1[e.getToken() & 255]); if (u & 1816 && (e.getToken() & 143360 || e.getToken() === -2147483528 || e.getToken() === -2147483527 ? d = K(e, t) : (e.getToken() & 134217728) == 134217728 ? d = q(e, t) : e.getToken() === 69271571 ? (u |= 2, d = $$1(e, t, r, 0)) : e.getToken() === 130 ? (u |= 8192, d = Yn(e, t, r, u)) : e.report(135)), u & 2 || (e.tokenValue === "constructor" ? ((e.getToken() & 1073741824) == 1073741824 ? e.report(129) : !(u & 32) && e.getToken() === 67174411 && (u & 920 ? e.report(53, "accessor") : t & 512 || (e.flags & 32 ? e.report(54) : e.flags |= 32)), u |= 64) : !(u & 8192) && u & 32 && e.tokenValue === "prototype" && e.report(52)), u & 1024 || e.getToken() !== 67174411 && !(u & 768)) return Xn(e, t, r, d, u, o, l); let p = Z$1(e, t | 16, r, u, c, e.tokenStart); return e.finishNode({ type: "MethodDefinition", kind: !(u & 32) && u & 64 ? "constructor" : u & 256 ? "get" : u & 512 ? "set" : "method", static: (u & 32) > 0, computed: (u & 2) > 0, key: d, value: p, ...e.options.next ? { decorators: o } : null }, l); } function Yn(e, t, n, r) { let { tokenStart: i } = e; A$1(e, t); let { tokenValue: a } = e; return a === "constructor" && e.report(128), e.options.lexical && (n || e.report(4, a), r ? n.addPrivateIdentifier(a, r) : n.addPrivateIdentifierRef(a)), A$1(e, t), e.finishNode({ type: "PrivateIdentifier", name: a }, i); } function Xn(e, t, n, r, i, a, o) { let s = null; if (i & 8 && e.report(0), e.getToken() === 1077936155) { A$1(e, t | 32); let { tokenStart: r } = e; e.getToken() === 537079927 && e.report(119); let a = 11264 | (i & 64 ? 0 : 16896); t = (t | a) ^ a | (i & 8 ? 1024 : 0) | (i & 16 ? 2048 : 0) | (i & 64 ? 16384 : 0) | 65792, s = G(e, t | 16, n, 2, 0, 1, 0, 1, r), ((e.getToken() & 1073741824) != 1073741824 || (e.getToken() & 4194304) == 4194304) && (s = W(e, t | 16, n, s, 0, 0, r), s = B$1(e, t | 16, n, 0, 0, r, s)); } return T$1(e, t), e.finishNode({ type: i & 1024 ? "AccessorProperty" : "PropertyDefinition", key: r, value: s, static: (i & 32) > 0, computed: (i & 2) > 0, ...e.options.next ? { decorators: a } : null }, o); } function Zn(e, t, n, r, i, a) { if (e.getToken() & 143360 || !(t & 1) && e.getToken() === -2147483527) return Qn(e, t, n, i, a); (e.getToken() & 2097152) != 2097152 && e.report(30, w$1[e.getToken() & 255]); let o = e.getToken() === 69271571 ? Y(e, t, n, r, 1, 0, 1, i, a) : Q(e, t, n, r, 1, 0, 1, i, a); return e.destructible & 16 && e.report(50), e.destructible & 32 && e.report(50), o; } function Qn(e, t, n, r, i) { let a = e.getToken(); t & 1 && ((a & 537079808) == 537079808 ? e.report(119) : ((a & 36864) == 36864 || a === -2147483527) && e.report(118)), (a & 20480) == 20480 && e.report(102), a === 241771 && (t & 1024 && e.report(32), t & 2 && e.report(111)), (a & 255) == 73 && r & 24 && e.report(100), a === 209006 && (t & 2048 && e.report(176), t & 2 && e.report(110)); let { tokenValue: o, tokenStart: s } = e; return A$1(e, t), n?.addVarOrBlock(t, o, r, i), e.finishNode({ type: "Identifier", name: o }, s); } function $n(e, t, n, r, i) { if (r || D$1(e, t, 8456256), e.getToken() === 8390721) { let a = er(e, i), [o, s] = ir(e, t, n, r); return e.finishNode({ type: "JSXFragment", openingFragment: a, children: o, closingFragment: s }, i); } e.getToken() === 8457014 && e.report(30, w$1[e.getToken() & 255]); let a = null, o = [], s = cr(e, t, n, r, i); if (!s.selfClosing) { [o, a] = rr(e, t, n, r); let i = We(a.name); We(s.name) !== i && e.report(155, i); } return e.finishNode({ type: "JSXElement", children: o, openingElement: s, closingElement: a }, i); } function er(e, t) { return j(e), e.finishNode({ type: "JSXOpeningFragment" }, t); } function tr(e, t, n, r) { D$1(e, t, 8457014); let i = lr(e, t); return e.getToken() !== 8390721 && e.report(25, w$1[65]), n ? j(e) : A$1(e, t), e.finishNode({ type: "JSXClosingElement", name: i }, r); } function nr(e, t, n, r) { return D$1(e, t, 8457014), e.getToken() !== 8390721 && e.report(25, w$1[65]), n ? j(e) : A$1(e, t), e.finishNode({ type: "JSXClosingFragment" }, r); } function rr(e, t, n, r) { let i = []; for (;;) { let a = ar(e, t, n, r); if (a.type === "JSXClosingElement") return [i, a]; i.push(a); } } function ir(e, t, n, r) { let i = []; for (;;) { let a = or(e, t, n, r); if (a.type === "JSXClosingFragment") return [i, a]; i.push(a); } } function ar(e, t, n, r) { if (e.getToken() === 137) return sr(e, t); if (e.getToken() === 2162700) return hr(e, t, n, 1, 0); if (e.getToken() === 8456256) { let { tokenStart: i } = e; return A$1(e, t), e.getToken() === 8457014 ? tr(e, t, r, i) : $n(e, t, n, 1, i); } e.report(0); } function or(e, t, n, r) { if (e.getToken() === 137) return sr(e, t); if (e.getToken() === 2162700) return hr(e, t, n, 1, 0); if (e.getToken() === 8456256) { let { tokenStart: i } = e; return A$1(e, t), e.getToken() === 8457014 ? nr(e, t, r, i) : $n(e, t, n, 1, i); } e.report(0); } function sr(e, t) { let n = e.tokenStart; A$1(e, t); let r = { type: "JSXText", value: e.tokenValue }; return e.options.raw && (r.raw = e.tokenRaw), e.finishNode(r, n); } function cr(e, t, n, r, i) { (e.getToken() & 143360) != 143360 && (e.getToken() & 4096) != 4096 && e.report(0); let a = lr(e, t), o = dr(e, t, n), s = e.getToken() === 8457014; return s && D$1(e, t, 8457014), e.getToken() !== 8390721 && e.report(25, w$1[65]), r || !s ? j(e) : A$1(e, t), e.finishNode({ type: "JSXOpeningElement", name: a, attributes: o, selfClosing: s }, i); } function lr(e, t) { let { tokenStart: n } = e; st(e); let r = vr(e, t); if (e.getToken() === 21) return mr(e, t, r, n); for (; E$1(e, t, 67108877);) st(e), r = ur(e, t, r, n); return r; } function ur(e, t, n, r) { let i = vr(e, t); return e.finishNode({ type: "JSXMemberExpression", object: n, property: i }, r); } function dr(e, t, n) { let r = []; for (; e.getToken() !== 8457014 && e.getToken() !== 8390721 && e.getToken() !== 1048576;) r.push(pr(e, t, n)); return r; } function fr(e, t, n) { let r = e.tokenStart; A$1(e, t), D$1(e, t, 14); let i = L$1(e, t, n, 1, 0, e.tokenStart); return D$1(e, t, 1074790415), e.finishNode({ type: "JSXSpreadAttribute", argument: i }, r); } function pr(e, t, n) { let { tokenStart: r } = e; if (e.getToken() === 2162700) return fr(e, t, n); st(e); let i = null, a = vr(e, t); if (e.getToken() === 21 && (a = mr(e, t, a, r)), e.getToken() === 1077936155) switch (at(e, t)) { case 134283267: i = q(e, t); break; case 8456256: i = $n(e, t, n, 0, e.tokenStart); break; case 2162700: i = hr(e, t, n, 0, 1); break; default: e.report(154); } return e.finishNode({ type: "JSXAttribute", value: i, name: a }, r); } function mr(e, t, n, r) { D$1(e, t, 21); let i = vr(e, t); return e.finishNode({ type: "JSXNamespacedName", namespace: n, name: i }, r); } function hr(e, t, n, r, i) { let { tokenStart: a } = e; A$1(e, t | 32); let { tokenStart: o } = e; if (e.getToken() === 14) return gr(e, t, n, a); let s = null; return e.getToken() === 1074790415 ? (i && e.report(157), s = _r(e, { index: e.startIndex, line: e.startLine, column: e.startColumn })) : s = L$1(e, t, n, 1, 0, o), e.getToken() !== 1074790415 && e.report(25, w$1[15]), r ? j(e) : A$1(e, t), e.finishNode({ type: "JSXExpressionContainer", expression: s }, a); } function gr(e, t, n, r) { D$1(e, t, 14); let i = L$1(e, t, n, 1, 0, e.tokenStart); return D$1(e, t, 1074790415), e.finishNode({ type: "JSXSpreadChild", expression: i }, r); } function _r(e, t) { return e.finishNode({ type: "JSXEmptyExpression" }, t, e.tokenStart); } function vr(e, t) { let n = e.tokenStart; e.getToken() & 143360 || e.report(30, w$1[e.getToken() & 255]); let { tokenValue: r } = e; return A$1(e, t), e.finishNode({ type: "JSXIdentifier", name: r }, n); } var yr = "6.1.4"; function br(e, t) { return ht(e, t); } function xr(e, t) { return ht(e, t, 3); } function Sr(e, t) { return ht(e, t); } var Cr = (function(e, t) { function n(e, t) { if (Array.isArray(t)) return Array.isArray(e) ? t.length === e.length && t.every((t, r) => n(e[r], t)) : !1; if (typeof t == "object") { if (!e) return !t; if ("or" in t) return t.or.some((t) => n(e, t)); if ("anykey" in t && Array.isArray(t.anykey)) { let r = Array.isArray(e) ? e : Object.values(e); return t.anykey.every((e) => r.some((t) => n(t, e))); } for (let [r, i] of Object.entries(t)) if (!n(e[r], i)) return !1; return !0; } return t === e; } function r(t) { return e.parse(t).body[0].expression; } let i = { or: [ { type: "ExpressionStatement", expression: { type: "AssignmentExpression", operator: "=", left: { or: [{ type: "Identifier" }, { type: "MemberExpression" }] }, right: { type: "FunctionExpression", async: !1 } } }, { type: "FunctionDeclaration", async: !1, id: { type: "Identifier" } }, { type: "VariableDeclaration", declarations: { anykey: [{ type: "VariableDeclarator", init: { type: "FunctionExpression", async: !1 } }] } } ] }, a = { type: "ExpressionStatement", expression: { type: "CallExpression", callee: { type: "MemberExpression", object: { type: "Identifier" }, property: {}, optional: !1 }, arguments: [{ type: "Literal", value: "alr" }, { type: "Literal", value: "yes" }], optional: !1 } }; function o(e) { if (!n(e, i)) return null; let t = []; if (e.type === "FunctionDeclaration") { let n = e.body?.body; e.id && n && t.push({ name: e.id, statements: n }); } else if (e.type === "ExpressionStatement") { if (e.expression.type !== "AssignmentExpression") return null; let n = e.expression.left, r = e.expression.right?.body?.body; n && r && t.push({ name: n, statements: r }); } else if (e.type === "VariableDeclaration") for (let n of e.declarations) { let e = n.id, r = n.init?.body?.body; e && r && t.push({ name: e, statements: r }); } for (let { name: e, statements: r } of t) if (n(r, { anykey: [a] })) return s(e); return null; } function s(e) { return r(` ({sig, n}) => { const url = (${t.generate(e)})("https://youtube.com/watch?v=yt-dlp-wins", "s", sig ? encodeURIComponent(sig) : undefined); url.set("n", n); const proto = Object.getPrototypeOf(url); const keys = Object.keys(proto).concat(Object.getOwnPropertyNames(proto)); for (const key of keys) { if (!["constructor", "set", "get", "clone"].includes(key)) { url[key](); break; } } const s = url.get("s"); return { sig: s ? decodeURIComponent(s) : null, n: url.get("n") ?? null, }; } `); } function c(n) { let r = e.parse(n), i = l(r), a = u(i); for (let [e, t] of Object.entries(a)) i.push({ type: "ExpressionStatement", expression: { type: "AssignmentExpression", operator: "=", left: { type: "MemberExpression", computed: !1, object: { type: "Identifier", name: "_result" }, property: { type: "Identifier", name: e }, optional: !1 }, right: f(t) } }); return t.generate(r); } function l(e) { let t = e.body, n = (() => { switch (t.length) { case 1: { let e = t[0]; if (e?.type === "ExpressionStatement" && e.expression.type === "CallExpression" && e.expression.callee.type === "MemberExpression" && e.expression.callee.object.type === "FunctionExpression") return e.expression.callee.object.body; break; } case 2: { let e = t[1]; if (e?.type === "ExpressionStatement" && e.expression.type === "CallExpression" && e.expression.callee.type === "FunctionExpression") { let t = e.expression.callee.body; return t.body.splice(0, 1), t; } break; } } throw Error("Unexpected YouTube player structure"); })(); return n.body = n.body.filter((e) => e.type !== "ExpressionStatement" || e.expression.type === "AssignmentExpression" || e.expression.type === "Literal"), n.body; } function u(e) { let t = { n: [], sig: [] }; for (let n of e) { let e = o(n); e && (t.n.push(d(e, { type: "Identifier", name: "n" })), t.sig.push(d(e, { type: "Identifier", name: "sig" }))); } return t; } function d(e, t) { return { type: "ArrowFunctionExpression", params: [t], body: { type: "MemberExpression", object: { type: "CallExpression", callee: e, arguments: [{ type: "ObjectExpression", properties: [{ type: "Property", key: t, value: t, kind: "init", computed: !1, method: !1, shorthand: !0 }] }], optional: !1 }, computed: !1, property: t, optional: !1 }, async: !1, expression: !0, generator: !1 }; } function f(e) { return r(` (_input) => { const _results = new Set(); const errors = []; for (const _generator of ${t.generate({ type: "ArrayExpression", elements: e })}) { try { _results.add(_generator(_input)); } catch (error) { errors.push(error); } } if (!_results.size) { throw new Error(\`no solutions: \${errors.join(", ")}\`); } if (_results.size !== 1) { throw new Error(\`invalid solutions: \${[..._results].map((value) => JSON.stringify(value)).join(", ")}\`); } return _results.values().next().value; } `); } return c; })(se, n$1); //#endregion //#region src/audioDownloader/strategies/safariPageBridge.ts var REQUEST_EVENT = "vot:safari-page-po-token:request"; var RESPONSE_EVENT = "vot:safari-page-po-token:response"; var BRIDGE_MARKER = "data-vot-safari-po-token-bridge"; var TOP_REQUEST_MESSAGE = "vot:safari-page-po-token:top-request"; var TOP_RESPONSE_MESSAGE = "vot:safari-page-po-token:top-response"; function isSafariBrowser() { return browserInfo.browser?.name === "Safari"; } function installSafariPageBridge() { if (!isSafariBrowser()) return false; if (document.documentElement.hasAttribute(BRIDGE_MARKER)) return true; const script = document.createElement("script"); const nonceSource = document.querySelector("script[nonce]"); if (nonceSource?.nonce) script.nonce = nonceSource.nonce; script.textContent = `(() => { const REQUEST_EVENT = ${JSON.stringify(REQUEST_EVENT)}; const RESPONSE_EVENT = ${JSON.stringify(RESPONSE_EVENT)}; const MARKER = ${JSON.stringify(BRIDGE_MARKER)}; if (document.documentElement.hasAttribute(MARKER)) return; document.documentElement.setAttribute(MARKER, "1"); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function mint(binding) { const keys = Object.getOwnPropertyNames(globalThis).filter( (key) => key === "bevasrsg" || key.startsWith("havuokmhhs-"), ); if (!keys.length) { throw new Error("YouTube PO token provider is unavailable in page realm"); } for (const key of keys) { let bevasrs; try { bevasrs = globalThis[key]?.bevasrs; } catch { continue; } const wpc = bevasrs?.wpc; if (typeof wpc !== "function") continue; for (let attempt = 0; attempt < 10; attempt++) { try { const minter = await wpc.call(bevasrs); const token = await minter?.mws?.({ c: binding, mc: false, me: false, }); if (typeof token === "string" && token) return token; } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!message.includes("SDF:notready")) break; } await delay(500); } } throw new Error("YouTube PO token mint failed in page realm"); } addEventListener(REQUEST_EVENT, async (event) => { const detail = event?.detail; const requestId = detail?.requestId; const binding = detail?.binding; if (typeof requestId !== "string" || typeof binding !== "string" || !binding) { return; } try { const token = await mint(binding); dispatchEvent(new CustomEvent(RESPONSE_EVENT, { detail: { requestId, token }, })); } catch (error) { dispatchEvent(new CustomEvent(RESPONSE_EVENT, { detail: { requestId, error: error instanceof Error ? error.message : String(error), }, })); } }); })();`; (document.documentElement || document.head).append(script); script.remove(); return document.documentElement.hasAttribute(BRIDGE_MARKER); } async function requestSafariPagePoTokenLocal(binding, signal) { if (!isSafariBrowser()) return void 0; signal.throwIfAborted(); if (!installSafariPageBridge()) { debug.error("[VOT][PO_TOKEN] Safari page bridge injection failed"); return; } const requestId = crypto.randomUUID(); debug.log("[VOT][PO_TOKEN] trying Safari page-realm bridge", { bindingLength: binding.length }); return await new Promise((resolve, reject) => { let settled = false; const timeout = setTimeout(() => finish(void 0), 8e3); const cleanup = () => { clearTimeout(timeout); removeEventListener(RESPONSE_EVENT, onResponse); signal.removeEventListener("abort", onAbort); }; const finish = (value) => { if (settled) return; settled = true; cleanup(); resolve(value); }; const onAbort = () => { if (settled) return; settled = true; cleanup(); reject(signal.reason); }; const onResponse = (event) => { const detail = event.detail; if (detail?.requestId !== requestId) return; if (typeof detail.token === "string" && detail.token) { finish(detail.token); return; } if (detail.error) debug.error("[VOT][PO_TOKEN] Safari page-realm mint failed", { message: detail.error }); finish(void 0); }; addEventListener(RESPONSE_EVENT, onResponse); signal.addEventListener("abort", onAbort, { once: true }); dispatchEvent(new CustomEvent(REQUEST_EVENT, { detail: { requestId, binding } })); }); } async function requestTopSafariPagePoToken(binding, signal) { const requestId = crypto.randomUUID(); debug.log("[VOT][PO_TOKEN] requesting token from top YouTube player", { bindingLength: binding.length }); return await new Promise((resolve, reject) => { let settled = false; const timeout = setTimeout(() => finish(void 0), 8e3); const cleanup = () => { clearTimeout(timeout); removeEventListener("message", onMessage); signal.removeEventListener("abort", onAbort); }; const finish = (value) => { if (settled) return; settled = true; cleanup(); resolve(value); }; const onAbort = () => { if (settled) return; settled = true; cleanup(); reject(signal.reason); }; const onMessage = (event) => { if (event.source !== window.top || event.origin !== location.origin) return; const data = event.data; if (data?.type !== TOP_RESPONSE_MESSAGE || data.requestId !== requestId) return; if (typeof data.token === "string" && data.token) { debug.log("[VOT][PO_TOKEN] token received from top YouTube player", { tokenLength: data.token.length }); finish(data.token); } else { if (data.error) debug.error("[VOT][PO_TOKEN] top player token mint failed", { message: data.error }); finish(void 0); } }; addEventListener("message", onMessage); signal.addEventListener("abort", onAbort, { once: true }); window.top?.postMessage({ type: TOP_REQUEST_MESSAGE, requestId, binding }, location.origin); }); } async function requestSafariPagePoToken(binding, signal) { if (!isSafariBrowser()) return void 0; signal.throwIfAborted(); if (window !== window.top) return requestTopSafariPagePoToken(binding, signal); return requestSafariPagePoTokenLocal(binding, signal); } //#endregion //#region src/audioDownloader/strategies/youtubePage.ts /** Resolve the real YouTube page realm when the userscript manager exposes it. */ function getMainWorldWindow(targetWindow) { try { if (typeof unsafeWindow !== "undefined" && unsafeWindow) { const unsafe = unsafeWindow; if (unsafe.document && unsafe.location?.hostname.endsWith("youtube.com")) return unsafe; } } catch {} try { const unsafe = globalThis.unsafeWindow; if (unsafe?.document && unsafe.location?.hostname.endsWith("youtube.com")) return unsafe; } catch {} try { const wrapped = targetWindow.wrappedJSObject; if (wrapped?.document && wrapped.location?.hostname.endsWith("youtube.com")) return wrapped; } catch {} return targetWindow; } /** Resolve the top same-origin YouTube page while preserving the native page realm. */ function getTopPageWindow(targetWindow) { const pageWindow = getMainWorldWindow(targetWindow); try { const top = pageWindow.top; if (top?.document && top.location?.hostname.endsWith("youtube.com")) try { return top.wrappedJSObject ?? top; } catch { return top; } } catch {} return pageWindow; } //#endregion //#region src/audioDownloader/strategies/youtubePoToken.ts /** Mint a GVS PO token in the native YouTube page realm when possible. */ async function mintPagePoToken(pageWindow, binding, signal) { const mainWindow = getTopPageWindow(pageWindow); const realms = /* @__PURE__ */ new Set([mainWindow]); if (mainWindow !== pageWindow) realms.add(pageWindow); try { realms.add(mainWindow.parent); realms.add(mainWindow.top); } catch {} debug.log("[VOT][PO_TOKEN] mint started", { bindingLength: binding.length, realmCount: realms.size }); for (const realm of realms) { let keys; try { keys = Object.getOwnPropertyNames(realm).filter((key) => key === "bevasrsg" || key.startsWith("havuokmhhs-")); } catch { continue; } for (const key of keys) { let bevasrs; try { bevasrs = realm[key]?.bevasrs; } catch { continue; } const wpc = bevasrs?.wpc; if (typeof wpc !== "function") continue; for (let attempt = 0; attempt < 10; attempt++) { if (signal.aborted) throw signal.reason; try { const token = await (await wpc.call(bevasrs))?.mws?.({ c: binding, mc: false, me: false }); if (typeof token === "string" && token) { debug.log("[VOT][PO_TOKEN] mint success", { tokenLength: token.length, provider: key, attempt }); return token; } debug.log("[VOT][PO_TOKEN] mint returned no token", { provider: key, attempt }); } catch (error) { const message = error instanceof Error ? error.message : String(error); debug.log("[VOT][PO_TOKEN] mint attempt failed", { provider: key, attempt, message }); if (!message.includes("SDF:notready")) break; } await createAbortableDelay(500, signal); } } } const safariToken = await requestSafariPagePoToken(binding, signal); if (safariToken) { debug.log("[VOT][PO_TOKEN] Safari page-realm mint success", { tokenLength: safariToken.length }); return safariToken; } debug.error("[VOT][PO_TOKEN] mint failed", { bindingLength: binding.length, realmCount: realms.size }); } /** Select the binding expected by YouTube's current GVS PO-token policy. */ function selectGvsPoTokenBinding(videoId, options) { if (options.experimentFlags.some((flags) => new URLSearchParams(flags).getAll("html5_generate_content_po_token").at(-1) === "true")) { debug.log("[VOT][WEB_CREATOR] selecting GVS PO binding", { kind: "video", loggedIn: options.loggedIn, hasDataSyncId: typeof options.dataSyncId === "string" && Boolean(options.dataSyncId), reason: "html5_generate_content_po_token" }); return { kind: "video", value: videoId }; } if (options.loggedIn && typeof options.dataSyncId === "string" && options.dataSyncId) { debug.log("[VOT][WEB_CREATOR] selecting GVS PO binding", { kind: "datasync", loggedIn: true, hasDataSyncId: true }); return { kind: "datasync", value: options.dataSyncId }; } if (typeof options.visitorData === "string" && options.visitorData) { debug.log("[VOT][WEB_CREATOR] selecting GVS PO binding", { kind: "visitor", loggedIn: options.loggedIn, hasDataSyncId: typeof options.dataSyncId === "string" && Boolean(options.dataSyncId) }); return { kind: "visitor", value: options.visitorData }; } debug.error("[VOT][WEB_CREATOR] no GVS PO binding available", { loggedIn: options.loggedIn, hasDataSyncId: typeof options.dataSyncId === "string" && Boolean(options.dataSyncId), hasVisitorData: typeof options.visitorData === "string" && Boolean(options.visitorData) }); } //#endregion //#region src/audioDownloader/strategies/webAbr.ts var MEDIA_RANGE_SIZES = [ 6e4, 8e4, 15e4, 33e4, 46e4 ]; async function fetchTvConfig(targetWindow, signal, videoId) { try { const response = await targetWindow.fetch("https://www.youtube.com/tv", { credentials: "omit", signal }); if (!response.ok) throw new Error(`Audio downloader. tv config request failed (${response.status})`); const html = await response.text(); const pick = (patterns) => { for (const pattern of patterns) { const match = pattern.exec(html); if (match?.[1]) return match[1]; } }; const playerPath = pick([/"PLAYER_JS_URL":"([^"]+)"/, /"jsUrl":"([^"]+)"/]); const sts = Number(pick([/"STS":(\d+)/, /"signatureTimestamp":(\d+)/])); const experimentFlags = []; for (const match of html.matchAll(/"serializedExperimentFlags"\s*:\s*("(?:\\.|[^"\\])*")/g)) try { experimentFlags.push(JSON.parse(match[1] ?? "\"\"")); } catch {} return { apiKey: pick([/"INNERTUBE_API_KEY":"([^"]+)"/]), clientVersion: pick([/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/]), visitorData: pick([/"VISITOR_DATA":"([^"]+)"/]), dataSyncId: pick([/"DATASYNC_ID":"([^"]+)"/]), experimentFlags, playerUrl: playerPath ? new URL(playerPath, "https://www.youtube.com").toString() : void 0, signatureTimestamp: Number.isFinite(sts) && sts > 0 ? sts : void 0 }; } catch (error) { signal.throwIfAborted(); debug.log("Audio downloader. client config unavailable", { videoId, client: "tv", error: error instanceof Error ? error.message : String(error) }); } } function buildMediaRanges(contentLength) { if (!Number.isInteger(contentLength) || contentLength < 1) return []; const ranges = []; let start = 0; let sizeIndex = 0; while (start < contentLength) { const size = MEDIA_RANGE_SIZES[sizeIndex] ?? MEDIA_RANGE_SIZES.at(-1) ?? 1; const end = Math.min(contentLength - 1, start + size - 1); ranges.push({ start, end }); start = end + 1; if (sizeIndex < MEDIA_RANGE_SIZES.length - 1) sizeIndex++; } return ranges; } function getConfigValue(config, key) { return config.get?.(key) ?? config.data_?.[key]; } function buildContentPlaybackContext$1(signatureTimestamp) { const context = { html5Preference: "HTML5_PREF_WANTS" }; const timestamp = Number(signatureTimestamp); if (Number.isFinite(timestamp) && timestamp > 0) context.signatureTimestamp = timestamp; return context; } function findJsonValueEnd(source, start) { const first = source[start]; if (first !== "{" && first !== "[" && first !== "\"") return -1; let depth = 0; let inString = false; let escaped = false; for (let index = start; index < source.length; index++) { const char = source[index]; if (inString) { if (escaped) escaped = false; else if (char === "\\") escaped = true; else if (char === "\"") { inString = false; if (depth === 0) return index + 1; } continue; } if (char === "\"") inString = true; else if (char === "{" || char === "[") depth++; else if ((char === "}" || char === "]") && --depth === 0) return index + 1; } return -1; } function parseYtcfgData(source) { const data = {}; const pattern = /ytcfg\s*\.\s*set\s*\(/g; const skipSpaces = (index) => { while (index < source.length && /\s/.test(source[index] ?? "")) index++; return index; }; for (let cursor = 0; cursor <= source.length;) { pattern.lastIndex = cursor; const match = pattern.exec(source); if (!match) break; cursor = match.index + match[0].length; const start = skipSpaces(cursor); const end = findJsonValueEnd(source, start); if (end < 0) continue; try { const argument = JSON.parse(source.slice(start, end)); if (argument && typeof argument === "object") { if (Array.isArray(argument)) continue; Object.assign(data, argument); cursor = end; continue; } if (typeof argument !== "string") continue; const separator = skipSpaces(end); if (source[separator] !== ",") continue; const valueStart = skipSpaces(separator + 1); const jsonEnd = findJsonValueEnd(source, valueStart); const valueEnd = jsonEnd < 0 ? source.indexOf(")", valueStart) : jsonEnd; if (valueEnd < 0) continue; data[argument] = JSON.parse(source.slice(valueStart, valueEnd).trim()); cursor = valueEnd; } catch {} } return data; } function readYtcfgFromDocument(targetWindow) { const data = {}; let scripts = []; try { scripts = [...targetWindow.document.querySelectorAll("script:not([src])")]; } catch { return data; } for (const script of scripts) { const source = script.textContent; if (!source?.includes("ytcfg")) continue; Object.assign(data, parseYtcfgData(source)); } return data; } async function resolveYtcfg(targetWindow, signal) { const pageConfig = targetWindow.ytcfg; if (pageConfig && typeof getConfigValue(pageConfig, "INNERTUBE_API_KEY") === "string") return pageConfig; let data = readYtcfgFromDocument(targetWindow); let source = "document"; if (typeof data.INNERTUBE_API_KEY !== "string") try { const response = await targetWindow.fetch(targetWindow.location.href, { credentials: "include", signal }); if (response.ok) { data = parseYtcfgData(await response.text()); source = "page"; } } catch (error) { signal.throwIfAborted(); debug.log("Audio downloader. web ABR config request failed", { error: error instanceof Error ? error.message : String(error) }); } if (typeof data.INNERTUBE_API_KEY !== "string") throw new Error("Audio downloader. web ABR config is unavailable"); debug.log("Audio downloader. web ABR config recovered", { source, hasContext: Boolean(data.INNERTUBE_CONTEXT), loggedIn: data.LOGGED_IN === true }); return { data_: data }; } function buildWebEmbeddedPlayerRequest(config, videoId, extractedSignatureTimestamp) { const rawContext = getConfigValue(config, "INNERTUBE_CONTEXT"); if (!rawContext || typeof rawContext !== "object") throw new Error("Audio downloader. web_embedded context is unavailable"); const context = JSON.parse(JSON.stringify(rawContext)); context.client ??= {}; const client = context.client; client.clientName = "WEB_EMBEDDED_PLAYER"; client.clientVersion = getConfigValue(config, "INNERTUBE_CLIENT_VERSION") ?? client.clientVersion; client.originalUrl = `https://www.youtube.com/embed/${videoId}?html5=1`; context.thirdParty ??= {}; context.thirdParty.embedUrl = "https://www.reddit.com/"; const contentPlaybackContext = buildContentPlaybackContext$1(extractedSignatureTimestamp ?? getConfigValue(config, "STS")); const encryptedHostFlags = getConfigValue(config, "WEB_PLAYER_CONTEXT_CONFIGS")?.WEB_PLAYER_CONTEXT_CONFIG_ID_EMBEDDED_PLAYER?.encryptedHostFlags; if (typeof encryptedHostFlags === "string" && encryptedHostFlags) contentPlaybackContext.encryptedHostFlags = encryptedHostFlags; return { context, videoId, playbackContext: { contentPlaybackContext }, contentCheckOk: true, racyCheckOk: true }; } function audioLanguageMatches(trackLanguage, requestedLanguage) { const track = normalizeAudioLanguageTag(trackLanguage); const requested = normalizeAudioLanguageTag(requestedLanguage); if (!track || !requested || requested === "auto") return false; if (track === requested) return true; return track.split("-")[0] === requested.split("-")[0]; } function isDrcAudioFormat(format) { if (typeof format.xtags === "string" && format.xtags.includes("drc=1")) return true; try { const cipher = typeof format.signatureCipher === "string" ? new URLSearchParams(format.signatureCipher) : void 0; const rawUrl = format.url ?? cipher?.get("url"); return (rawUrl ? new URL(rawUrl).searchParams.get("xtags") : null)?.includes("drc=1") === true; } catch { return false; } } function selectWebEmbeddedAudioFormat(formats, requestedLanguage) { const audioOnly = formats.filter(({ url, signatureCipher }) => typeof url === "string" || typeof signatureCipher === "string").filter(({ mimeType }) => mimeType?.includes("audio/") && !mimeType?.includes("video/")); const normalizedRequestedLanguage = normalizeAudioLanguageTag(requestedLanguage); const exactLanguageCandidates = normalizedRequestedLanguage && normalizedRequestedLanguage !== "auto" ? audioOnly.filter((format) => getYoutubeAudioFormatLanguage(format) === normalizedRequestedLanguage) : []; const requestedLanguageCandidates = exactLanguageCandidates.length > 0 ? exactLanguageCandidates : normalizedRequestedLanguage && normalizedRequestedLanguage !== "auto" ? audioOnly.filter((format) => audioLanguageMatches(getYoutubeAudioFormatLanguage(format), normalizedRequestedLanguage)) : []; const defaultAudioOnly = audioOnly.filter(({ audioTrack }) => audioTrack?.audioIsDefault === true); const trackCandidates = requestedLanguageCandidates.length > 0 ? requestedLanguageCandidates : defaultAudioOnly.length > 0 ? defaultAudioOnly : audioOnly; const nonDrcCandidates = trackCandidates.filter((format) => !isDrcAudioFormat(format)); const selected = selectSmallestAudioFormat(nonDrcCandidates.length > 0 ? nonDrcCandidates : trackCandidates); if (!selected) throw new Error("Audio downloader. web ABR returned no direct audio-only formats"); return selected; } async function sha1(value) { const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(value)); return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } async function buildSidAuthorization(scheme, sid, origin, timestamp, userSessionId) { return `${scheme} ${timestamp}_${await sha1(userSessionId ? `${userSessionId} ${timestamp} ${sid} ${origin}` : `${timestamp} ${sid} ${origin}`)}${userSessionId ? "_u" : ""}`; } async function getYouTubeAuthorization(targetWindow, userSessionId) { const cookies = new Map(targetWindow.document.cookie.split("; ").map((cookie) => { const separator = cookie.indexOf("="); return separator < 0 ? [cookie, ""] : [cookie.slice(0, separator), cookie.slice(separator + 1)]; })); const timestamp = String(Math.round(Date.now() / 1e3)); const origin = "https://www.youtube.com"; return (await Promise.all([ ["SAPISIDHASH", cookies.get("SAPISID") ?? cookies.get("__Secure-3PAPISID")], ["SAPISID1PHASH", cookies.get("__Secure-1PAPISID")], ["SAPISID3PHASH", cookies.get("__Secure-3PAPISID")] ].map(async ([scheme, sid]) => sid ? buildSidAuthorization(scheme, sid, origin, timestamp, userSessionId || void 0) : ""))).filter(Boolean).join(" ") || void 0; } function getPlayerUrl(config) { const playerContexts = getConfigValue(config, "WEB_PLAYER_CONTEXT_CONFIGS"); const value = getConfigValue(config, "PLAYER_JS_URL") ?? getConfigValue(config, "JS_URL") ?? playerContexts?.WEB_PLAYER_CONTEXT_CONFIG_ID_EMBEDDED_PLAYER?.jsUrl; return typeof value === "string" ? new URL(value, "https://www.youtube.com").toString() : void 0; } function resolveTrustedRealm(realm) { const candidates = [realm]; const add = (candidate) => { if (candidate && candidate !== realm) candidates.push(candidate); }; try { add(realm.parent); } catch {} try { add(realm.top); } catch {} for (const candidate of candidates) try { if (candidate.trustedTypes?.createPolicy) return candidate; } catch {} return realm; } function runChallengeSolver(realm, preparedPlayer, signature, n) { const nativeRealm = resolveTrustedRealm(realm); const policy = nativeRealm.trustedTypes?.createPolicy(`vot-youtube-solver-${crypto.randomUUID()}`, { createScript: (value) => value }); const source = `(function(){\nconst _result={sig:null,n:null};\n${preparedPlayer}\nreturn _result;\n})()`; const script = policy?.createScript(source) ?? source; const result = nativeRealm.eval(script); if (!result) throw new Error("Audio downloader. YouTube challenge solver returned none"); const solved = { signature: signature && result.sig ? result.sig(signature) : void 0, n: n && result.n ? result.n(n) : void 0 }; if (signature && !solved.signature || n && !solved.n) throw new Error("Audio downloader. YouTube challenge solve incomplete"); return solved; } var SIG_PATTERN = /^[A-Za-z0-9_-]{20,}={0,2}$/; var N_PATTERN$1 = /^[A-Za-z0-9_-]{4,}$/; function listPageFunctions(pageWindow) { const found = []; const seen = /* @__PURE__ */ new Set(); let visited = 0; const visit = (value, path, depth) => { if (!value || seen.has(value) || depth > 3 || visited++ >= 5e3) return; seen.add(value); if (typeof value === "function") found.push({ fn: value, path }); else if (typeof value === "object") try { for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) if ("value" in descriptor) visit(descriptor.value, `${path}.${key}`, depth + 1); } catch {} }; try { const descriptors = Object.getOwnPropertyDescriptors(pageWindow); visit(descriptors._yt_player?.value, "_yt_player", 0); for (const [key, descriptor] of Object.entries(descriptors)) if (typeof descriptor.value === "function") visit(descriptor.value, key, 0); } catch {} return found; } var SIG_FACTORY_NEW_PATTERN = /([A-Za-z_$][\w$]*)\s*=\s*new\s+[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*\s*\(\s*\1\s*,\s*(?:!\s*0|true)\s*\)\s*;\s*\1\.set\(\s*["']alr["']\s*,\s*["']yes["']\s*\)/; var EJS_MOCK_URL = "https://youtube.com/watch?v=yt-dlp-wins"; function isSigFactory({ fn }) { try { return SIG_FACTORY_NEW_PATTERN.test(Function.prototype.toString.call(fn)); } catch { return false; } } function pageUrlMethods(proto) { if (!proto) return; const descriptors = /* @__PURE__ */ new Map(); for (let current = proto; current; current = Object.getPrototypeOf(current)) for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(current))) if (!descriptors.has(key)) descriptors.set(key, descriptor); const get = descriptors.get("get")?.value; const set = descriptors.get("set")?.value; if (typeof get !== "function" || typeof set !== "function" || typeof descriptors.get("clone")?.value !== "function") return; return { get, set, transforms: [...descriptors].flatMap(([key, descriptor]) => { if ([ "constructor", "set", "get", "clone" ].includes(key)) return []; const method = descriptor.value; if (typeof method !== "function") return []; const source = Function.prototype.toString.call(method); return /\.set\(\s*["']n["']\s*,/.test(source) || /for\s*\([^)]*\bof\b[^)]*\.params\b/.test(source) && /\.params\.set\(/.test(source) ? [method] : []; }) }; } function validPageValue(value, input, pattern) { if (!input || typeof value !== "string") return; let decoded = value; for (let index = 0; index < 3 && decoded.includes("%"); index++) try { decoded = decodeURIComponent(decoded); } catch { return; } return decoded !== input && pattern.test(decoded) ? decoded : void 0; } function collectPageSolutions(pageWindow, challenge) { const realms = /* @__PURE__ */ new Set([pageWindow]); for (const relation of ["parent", "top"]) try { const other = pageWindow[relation]; if (other) realms.add(other); } catch {} const solutions = []; const seen = /* @__PURE__ */ new Set(); const collect = (instance, methods, transform, factory) => { const solution = {}; const readSignature = () => { if (!challenge.signature) return; const keys = factory ? ["s"] : ["s", challenge.sp]; for (const key of keys) { if (!key) continue; const value = validPageValue(methods.get.call(instance, key), challenge.signature, SIG_PATTERN); if (value) return value; } }; if (challenge.signature) try { solution.signature = readSignature(); } catch {} if (challenge.n && transform) try { if (factory) methods.set.call(instance, "n", challenge.n); solution.n = validPageValue(methods.get.call(instance, "n"), challenge.n, N_PATTERN$1); if (!solution.n) { transform.call(instance); solution.n = validPageValue(methods.get.call(instance, "n"), challenge.n, N_PATTERN$1); if (!solution.signature) solution.signature = readSignature(); } } catch {} if (solution.signature || solution.n) solutions.push(solution); }; for (const realm of realms) for (const entry of listPageFunctions(realm)) { const { fn, path } = entry; if (seen.has(fn)) continue; seen.add(fn); try { if (isSigFactory(entry)) { const make = () => fn(EJS_MOCK_URL, "s", encodeURIComponent(challenge.signature ?? "")); const instance = make(); if (!instance || typeof instance !== "object") continue; const methods = pageUrlMethods(Object.getPrototypeOf(instance)); if (!methods) continue; collect(instance, methods, methods.transforms[0], true); if (challenge.n) for (const transform of methods.transforms.slice(1)) collect(make(), methods, transform, true); } else if (challenge.n && path.startsWith("_yt_player.")) { const proto = Object.getOwnPropertyDescriptor(fn, "prototype")?.value; const methods = pageUrlMethods(proto); if (!methods?.transforms.length) continue; const UrlCtor = fn; for (const transform of methods.transforms) try { collect(new UrlCtor(challenge.url, true), methods, transform, false); } catch {} } } catch {} } const consensus = {}; for (const field of ["signature", "n"]) { const values = new Set(solutions.map((solution) => solution[field]).filter(Boolean)); if (values.size === 1) consensus[field] = values.values().next().value; } const merged = /* @__PURE__ */ new Map(); for (const solution of solutions) { const candidate = { signature: solution.signature ?? consensus.signature, n: solution.n ?? consensus.n }; merged.set(JSON.stringify(candidate), candidate); } return [...merged.values()]; } function solveYouTubeChallenges(targetWindow, playerCode, signature, n) { const preparedPlayer = Cr(playerCode); const errors = []; try { return runChallengeSolver(targetWindow, preparedPlayer, signature, n); } catch (error) { errors.push(error instanceof Error ? error.message : String(error)); } const sandbox = targetWindow.document.createElement("iframe"); sandbox.style.display = "none"; sandbox.setAttribute("aria-hidden", "true"); sandbox.setAttribute("sandbox", "allow-scripts allow-same-origin"); (targetWindow.document.body ?? targetWindow.document.documentElement).append(sandbox); try { const realm = sandbox.contentWindow; if (!realm) throw new Error("Challenge solver sandbox is unavailable"); return runChallengeSolver(realm, preparedPlayer, signature, n); } catch (error) { errors.push(error instanceof Error ? error.message : String(error)); } finally { sandbox.remove(); } throw new Error(`Audio downloader. YouTube challenge solve failed (${errors.join(" | ")})`); } function buildSolvedUrl(rawUrl, sp, solved) { const url = new URL(rawUrl); if (solved.signature) url.searchParams.set(sp ?? "signature", solved.signature); if (solved.n) url.searchParams.set("n", solved.n); return url.toString(); } async function* resolveWebEmbeddedFormatUrl(targetWindow, format, playerCode, signal) { signal.throwIfAborted(); const cipher = format.signatureCipher ? new URLSearchParams(format.signatureCipher) : void 0; const rawUrl = format.url ?? cipher?.get("url"); if (!rawUrl) throw new Error("Audio downloader. web ABR format URL is unavailable"); const url = new URL(rawUrl); const signature = cipher?.get("s") ?? void 0; const n = url.searchParams.get("n") ?? void 0; if (!signature && !n) { yield url.toString(); signal.throwIfAborted(); return; } const challenge = { url: rawUrl, sp: cipher?.get("sp") ?? void 0, signature, n }; const candidates = collectPageSolutions(targetWindow, challenge); signal.throwIfAborted(); const complete = (solution) => (!signature || !!solution.signature) && (!n || !!solution.n); candidates.sort((a, b) => Number(complete(b)) - Number(complete(a))); let source; const astSolutions = /* @__PURE__ */ new Map(); const solve = async (signature, n) => { signal.throwIfAborted(); const key = JSON.stringify([signature, n]); const cached = astSolutions.get(key); if (cached) return cached; source ??= playerCode(); const code = await source; signal.throwIfAborted(); if (!code) throw new Error("Audio downloader. YouTube player code is unavailable"); const raw = solveYouTubeChallenges(targetWindow, code, signature, n); signal.throwIfAborted(); const solved = { signature: validPageValue(raw.signature, signature, SIG_PATTERN), n: validPageValue(raw.n, n, N_PATTERN$1) }; if (signature && !solved.signature || n && !solved.n) throw new Error("Audio downloader. YouTube challenge solve invalid"); astSolutions.set(key, solved); return solved; }; const yielded = /* @__PURE__ */ new Set(); const errors = []; for (const candidate of candidates) { signal.throwIfAborted(); let solved = candidate; try { if (!complete(candidate)) { const missing = await solve(candidate.signature ? void 0 : signature, candidate.n ? void 0 : n); solved = { signature: candidate.signature ?? missing.signature, n: candidate.n ?? missing.n }; } } catch (error) { signal.throwIfAborted(); errors.push(error instanceof Error ? error.message : String(error)); continue; } signal.throwIfAborted(); const candidateUrl = buildSolvedUrl(rawUrl, challenge.sp, solved); if (!yielded.has(candidateUrl)) { yielded.add(candidateUrl); yield candidateUrl; } } signal.throwIfAborted(); let solved; try { solved = await solve(signature, n); } catch (error) { signal.throwIfAborted(); errors.push(error instanceof Error ? error.message : String(error)); throw new Error(`Audio downloader. challenge solve failed (${errors.join(" | ")})`); } signal.throwIfAborted(); const fallbackUrl = buildSolvedUrl(rawUrl, challenge.sp, solved); if (!yielded.has(fallbackUrl)) yield fallbackUrl; signal.throwIfAborted(); } function buildTvDowngradedPlayerRequest(videoId, options = {}) { const contentPlaybackContext = buildContentPlaybackContext$1(options.signatureTimestamp); return { context: { client: { clientName: "TVHTML5", clientVersion: typeof options.clientVersion === "string" && options.clientVersion ? options.clientVersion : "5.20260707", hl: "en", gl: "US", timeZone: "UTC", utcOffsetMinutes: 0, userAgent: "Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version", ...typeof options.visitorData === "string" ? { visitorData: options.visitorData } : {} } }, videoId, playbackContext: { contentPlaybackContext }, contentCheckOk: true, racyCheckOk: true }; } function buildWebPlayerRequest(config, videoId, extractedSignatureTimestamp) { const rawContext = getConfigValue(config, "INNERTUBE_CONTEXT"); if (!rawContext || typeof rawContext !== "object") throw new Error("Audio downloader. web client context is unavailable"); const context = JSON.parse(JSON.stringify(rawContext)); context.client ??= {}; const client = context.client; client.clientName = "WEB"; client.clientVersion = getConfigValue(config, "INNERTUBE_CLIENT_VERSION") ?? client.clientVersion; client.originalUrl = `https://www.youtube.com/watch?v=${videoId}`; delete context.thirdParty; return { context, videoId, playbackContext: { contentPlaybackContext: buildContentPlaybackContext$1(extractedSignatureTimestamp ?? getConfigValue(config, "STS")) }, contentCheckOk: true, racyCheckOk: true }; } function buildWebCreatorPlayerRequest(videoId, options = {}) { const contentPlaybackContext = buildContentPlaybackContext$1(options.signatureTimestamp); return { context: { client: { clientName: "WEB_CREATOR", clientVersion: typeof options.clientVersion === "string" && options.clientVersion ? options.clientVersion : "1.20260708.06.00", hl: "en", gl: "US", timeZone: "UTC", utcOffsetMinutes: 0, ...typeof options.visitorData === "string" ? { visitorData: options.visitorData } : {} } }, videoId, playbackContext: { contentPlaybackContext }, contentCheckOk: true, racyCheckOk: true }; } async function postInnertubePlayer(targetWindow, signal, apiKey, body, clientName, clientVersion, extra) { const visitorData = body.context?.client?.visitorData; const authenticated = Boolean(extra.authorization); const response = await targetWindow.fetch(`https://www.youtube.com/youtubei/v1/player?prettyPrint=false&key=${encodeURIComponent(apiKey)}`, { method: "POST", credentials: authenticated ? "include" : "omit", signal, headers: { "content-type": "application/json", "x-youtube-client-name": clientName, "x-youtube-client-version": clientVersion, ...typeof visitorData === "string" ? { "x-goog-visitor-id": visitorData } : {}, ...authenticated ? { authorization: extra.authorization, "x-origin": "https://www.youtube.com", "x-youtube-bootstrap-logged-in": "true", ...typeof extra.sessionIndex === "number" || typeof extra.sessionIndex === "string" ? { "x-goog-authuser": String(extra.sessionIndex) } : {}, ...typeof extra.delegatedSessionId === "string" && extra.delegatedSessionId ? { "x-goog-pageid": extra.delegatedSessionId } : {} } : {} }, body: JSON.stringify(body) }); if (!response.ok) throw new Error(`Audio downloader. player request failed (${response.status})`); return await response.json(); } async function probeContentLength(targetWindow, streamUrl, signal) { const url = new URL(streamUrl); url.searchParams.set("range", "0-0"); url.searchParams.delete("ump"); const response = await targetWindow.fetch(url, { signal }); if (!response.ok) throw new Error(`Audio downloader. web ABR media probe failed (${response.status})`); const total = Number(/\/(\d+)\s*$/.exec(response.headers.get("content-range") ?? "")?.[1]); if (!(total > 0)) throw new Error("Audio downloader. web ABR content length unknown"); return total; } var WEB_ABR_TRANSPORTS = [ "parallel_4", "4mb", "parallel_8", "8mb", "parallel_2", "2mb", "stream", "original" ]; function makeFixedRanges(contentLength, chunkSize) { const ranges = []; for (let start = 0; start < contentLength; start += chunkSize) ranges.push({ start, end: Math.min(contentLength - 1, start + chunkSize - 1) }); return ranges; } var WEB_ABR_RANGE_MAX_ATTEMPTS = 10; var WEB_ABR_RANGE_REFRESH_EVERY_FAILURES = 2; var WEB_ABR_RANGE_RETRY_BASE_DELAY_MS = 250; var WEB_ABR_RANGE_RETRY_MAX_DELAY_MS = 1500; var WEB_ABR_FATAL_MEDIA_STATUSES = /* @__PURE__ */ new Set([ 401, 403, 404, 410 ]); var MediaHttpError = class extends Error { status; constructor(status, message) { super(message); this.status = status; this.name = "MediaHttpError"; } }; function isFatalMediaError(error) { return error instanceof MediaHttpError && WEB_ABR_FATAL_MEDIA_STATUSES.has(error.status); } async function refreshMediaUrl(urlState, refreshUrl, reason = null) { if (!urlState.refreshPromise) { const previousUrl = urlState.value; const previousVersion = urlState.version ?? 0; urlState.refreshPromise = Promise.resolve().then(() => refreshUrl()).then((nextUrl) => { if (typeof nextUrl !== "string" || !nextUrl) throw new Error("Audio downloader. Failed to refresh media URL"); urlState.value = nextUrl; urlState.version = previousVersion + 1; debug.log("Audio downloader. web ABR media URL refresh applied", { reason, version: urlState.version, urlChanged: nextUrl !== previousUrl }); return nextUrl; }).finally(() => { urlState.refreshPromise = null; }); } return await urlState.refreshPromise; } async function fetchMediaRange(targetWindow, urlState, start, end, signal, refreshUrl, requestNumberRef) { let lastError; let refreshedFatal = false; for (let attempt = 0; attempt < WEB_ABR_RANGE_MAX_ATTEMPTS; attempt++) { signal.throwIfAborted(); if (attempt > 0 && urlState.refreshPromise) await urlState.refreshPromise; try { const urlVersion = urlState.version ?? 0; const url = new URL(urlState.value); url.searchParams.set("range", `${start}-${end}`); url.searchParams.set("rn", String(++requestNumberRef.value)); url.searchParams.delete("ump"); const response = await targetWindow.fetch(url, { signal, cache: "no-store" }); if (!response.ok) throw new MediaHttpError(response.status, `Audio downloader. Media request failed (${response.status}, range ${start}-${end})`); const bytes = new Uint8Array(await response.arrayBuffer()); signal.throwIfAborted(); if (bytes.byteLength === end - start + 1) { if (attempt > 0) debug.log("Audio downloader. web ABR range recovered", { range: `${start}-${end}`, attempt: attempt + 1, maxAttempts: WEB_ABR_RANGE_MAX_ATTEMPTS, urlVersion }); return bytes; } const redirect = new TextDecoder("ascii").decode(bytes).match(/^\s*(https:\/\/\S+)\s*$/)?.[1]; if (redirect) { const next = new URL(redirect); if (!/(?:^|\.)googlevideo\.com$/.test(next.hostname)) throw new Error("Audio downloader. Invalid media redirect"); urlState.value = next.toString(); if (attempt + 1 < WEB_ABR_RANGE_MAX_ATTEMPTS) continue; } throw new Error(`Audio downloader. Incomplete web ABR chunk (${bytes.byteLength}/${end - start + 1}, range ${start}-${end})`); } catch (error) { signal.throwIfAborted(); lastError = error; const failedAttempt = attempt + 1; const fatal = isFatalMediaError(error); const hasMoreAttempts = failedAttempt < WEB_ABR_RANGE_MAX_ATTEMPTS; const shouldRefreshUrl = hasMoreAttempts && (fatal || failedAttempt % WEB_ABR_RANGE_REFRESH_EVERY_FAILURES === 0); debug.log("Audio downloader. web ABR range request failed", { range: `${start}-${end}`, attempt: failedAttempt, maxAttempts: WEB_ABR_RANGE_MAX_ATTEMPTS, fatal, refreshUrl: shouldRefreshUrl, error: error instanceof Error ? error.message : String(error) }); if (!hasMoreAttempts) break; if (fatal && refreshedFatal) break; await createAbortableDelay(Math.min(WEB_ABR_RANGE_RETRY_BASE_DELAY_MS * failedAttempt, WEB_ABR_RANGE_RETRY_MAX_DELAY_MS), signal); if (shouldRefreshUrl) try { await refreshMediaUrl(urlState, refreshUrl, { range: `${start}-${end}`, failedAttempt }); if (fatal) refreshedFatal = true; debug.log("Audio downloader. web ABR media URL refreshed for range retry", { range: `${start}-${end}`, nextAttempt: failedAttempt + 1, urlVersion: urlState.version ?? 0 }); } catch (refreshError) { signal.throwIfAborted(); debug.log("Audio downloader. web ABR media URL refresh failed", { range: `${start}-${end}`, nextAttempt: failedAttempt + 1, error: refreshError instanceof Error ? refreshError.message : String(refreshError) }); if (fatal) break; lastError = refreshError; } } } throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Audio downloader. Media range failed"); } async function* emitOrderedBuffers(buffers, isLastBatch, pendingState) { for (let bufferIndex = 0; bufferIndex < buffers.length; bufferIndex++) { const buffer = buffers[bufferIndex]; pendingState.buffers.push(buffer); pendingState.size += buffer.byteLength; const isFinalBuffer = isLastBatch && bufferIndex === buffers.length - 1; if (pendingState.size >= config_default$1.minChunkSize && !isFinalBuffer) { yield { buffer: concatBuffers(pendingState.buffers), isLastChunk: false }; pendingState.buffers = []; pendingState.size = 0; } } if (isLastBatch) { if (pendingState.size < 1) throw new Error("Audio downloader. Final web ABR chunk is empty"); yield { buffer: concatBuffers(pendingState.buffers), isLastChunk: true }; pendingState.buffers = []; pendingState.size = 0; } } async function* downloadRangesSequential(targetWindow, streamUrl, _contentLength, signal, refreshUrl, ranges) { const urlState = { value: streamUrl, refreshPromise: null, version: 0 }; const requestNumberRef = { value: 0 }; const pendingState = { buffers: [], size: 0 }; for (let index = 0; index < ranges.length; index++) { const { start, end } = ranges[index]; const buffer = await fetchMediaRange(targetWindow, urlState, start, end, signal, refreshUrl, requestNumberRef); for await (const chunk of emitOrderedBuffers([buffer], index === ranges.length - 1, pendingState)) yield chunk; } } async function* downloadRangesParallel(targetWindow, streamUrl, contentLength, signal, refreshUrl, concurrency) { const ranges = makeFixedRanges(contentLength, 4194304); const urlState = { value: streamUrl, refreshPromise: null, version: 0 }; const requestNumberRef = { value: 0 }; const pendingState = { buffers: [], size: 0 }; for (let index = 0; index < ranges.length; index += concurrency) { signal.throwIfAborted(); const batch = ranges.slice(index, index + concurrency); const buffers = await Promise.all(batch.map(({ start, end }) => fetchMediaRange(targetWindow, urlState, start, end, signal, refreshUrl, requestNumberRef))); for await (const chunk of emitOrderedBuffers(buffers, index + batch.length >= ranges.length, pendingState)) yield chunk; } } async function* downloadStream(targetWindow, streamUrl, signal) { const url = new URL(streamUrl); url.searchParams.delete("range"); url.searchParams.delete("rn"); url.searchParams.delete("ump"); const response = await targetWindow.fetch(url, { signal }); if (!response.ok) throw new Error(`Audio downloader. Stream request failed (${response.status})`); if (!response.body) throw new Error("Audio downloader. Stream body is unavailable"); const reader = response.body.getReader(); const pending = []; let pendingSize = 0; let readyChunk = null; try { for (;;) { signal.throwIfAborted(); const { value, done } = await reader.read(); if (done) break; if (!value?.byteLength) continue; const bytes = value instanceof Uint8Array ? value : new Uint8Array(value); pending.push(bytes); pendingSize += bytes.byteLength; if (pendingSize >= config_default$1.minChunkSize) { const nextChunk = concatBuffers(pending); pending.length = 0; pendingSize = 0; if (readyChunk) yield { buffer: readyChunk, isLastChunk: false }; readyChunk = nextChunk; } } } finally { try { reader.releaseLock(); } catch {} } if (pendingSize > 0) { if (readyChunk) yield { buffer: readyChunk, isLastChunk: false }; yield { buffer: concatBuffers(pending), isLastChunk: true }; return; } if (!readyChunk?.byteLength) throw new Error("Audio downloader. Stream ended without audio data"); yield { buffer: readyChunk, isLastChunk: true }; } async function* downloadWithTransport(targetWindow, transport, streamUrl, contentLength, signal, refreshUrl) { switch (transport) { case "parallel_4": yield* downloadRangesParallel(targetWindow, streamUrl, contentLength, signal, refreshUrl, 4); return; case "parallel_2": yield* downloadRangesParallel(targetWindow, streamUrl, contentLength, signal, refreshUrl, 2); return; case "parallel_8": yield* downloadRangesParallel(targetWindow, streamUrl, contentLength, signal, refreshUrl, 8); return; case "stream": yield* downloadStream(targetWindow, streamUrl, signal); return; case "8mb": yield* downloadRangesSequential(targetWindow, streamUrl, contentLength, signal, refreshUrl, makeFixedRanges(contentLength, 8388608)); return; case "4mb": yield* downloadRangesSequential(targetWindow, streamUrl, contentLength, signal, refreshUrl, makeFixedRanges(contentLength, 4194304)); return; case "2mb": yield* downloadRangesSequential(targetWindow, streamUrl, contentLength, signal, refreshUrl, makeFixedRanges(contentLength, 2097152)); return; case "original": yield* downloadRangesSequential(targetWindow, streamUrl, contentLength, signal, refreshUrl, buildMediaRanges(contentLength)); return; default: throw new Error(`Audio downloader. Unknown web ABR transport: ${transport}`); } } async function* downloadMediaRanges(targetWindow, streamUrl, contentLength, signal, refreshUrl) { if (!Number.isSafeInteger(contentLength) || contentLength < 1) throw new Error("Audio downloader. Invalid media content length"); const transports = [...WEB_ABR_TRANSPORTS]; debug.log("Audio downloader. web ABR transport order", { transports, bufferBeforeEmit: true }); let lastError; for (const transport of transports) { signal.throwIfAborted(); const startedAt = performance.now(); try { debug.log("Audio downloader. web ABR transport started", { transport, contentLength, bufferBeforeEmit: true }); const bufferedChunks = []; let downloadedBytes = 0; for await (const chunk of downloadWithTransport(targetWindow, transport, streamUrl, contentLength, signal, refreshUrl)) { if (!chunk?.buffer?.byteLength) throw new Error("Audio downloader. Web ABR transport produced an empty chunk"); bufferedChunks.push(chunk); downloadedBytes += chunk.buffer.byteLength; } if (downloadedBytes !== contentLength) throw new Error(`Audio downloader. Incomplete web ABR download (${downloadedBytes}/${contentLength} bytes)`); if (bufferedChunks.length < 1) throw new Error("Audio downloader. Web ABR transport returned no audio chunks"); for (let index = 0; index < bufferedChunks.length; index++) bufferedChunks[index] = { ...bufferedChunks[index], isLastChunk: index === bufferedChunks.length - 1 }; debug.log("Audio downloader. web ABR transport fully buffered", { transport, chunks: bufferedChunks.length, downloadedBytes, elapsedMs: Math.round(performance.now() - startedAt) }); for (const chunk of bufferedChunks) yield chunk; debug.log("Audio downloader. web ABR transport finished", { transport, elapsedMs: Math.round(performance.now() - startedAt), bufferBeforeEmit: true }); return; } catch (error) { signal.throwIfAborted(); lastError = error; debug.log("Audio downloader. web ABR transport failed", { transport, emitted: false, bufferBeforeEmit: true, elapsedMs: Math.round(performance.now() - startedAt), error: error instanceof Error ? error.message : String(error) }); if (isFatalMediaError(error)) { debug.log("Audio downloader. web ABR transport matrix aborted", { transport, error: error instanceof Error ? error.message : String(error) }); throw error; } } } throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Audio downloader. All web ABR transports failed"); } async function* getWebAbrAudioChunks$1(targetWindow, videoId, signal, sourceLanguage) { const config = await resolveYtcfg(targetWindow, signal); const apiKey = getConfigValue(config, "INNERTUBE_API_KEY"); if (typeof apiKey !== "string") throw new Error("Audio downloader. web ABR config is unavailable"); const playerCodes = /* @__PURE__ */ new Map(); const fetchPlayerCode = (url = getPlayerUrl(config)) => { if (!url) return Promise.resolve(void 0); let code = playerCodes.get(url); if (!code) { code = targetWindow.fetch(url, { signal }).then((response) => { if (!response.ok) throw new Error(`Audio downloader. YouTube player request failed (${response.status})`); return response.text(); }); playerCodes.set(url, code); } return code; }; let sts = Number(getConfigValue(config, "STS")); if (!(sts > 0)) sts = Number((await fetchPlayerCode())?.match(/(?:signatureTimestamp|sts)\s*:\s*([0-9]{5})/)?.[1]); const body = buildWebEmbeddedPlayerRequest(config, videoId, sts); const context = body.context; const clientVersion = String(context.client.clientVersion ?? ""); const visitorData = context.client.visitorData ?? getConfigValue(config, "VISITOR_DATA"); if (typeof visitorData === "string") context.client.visitorData = visitorData; const dataSyncId = getConfigValue(config, "DATASYNC_ID"); const [firstSyncId, secondSyncId] = typeof dataSyncId === "string" ? dataSyncId.split("||") : []; const delegatedSessionId = getConfigValue(config, "DELEGATED_SESSION_ID") ?? (secondSyncId ? firstSyncId : void 0); const authorization = await getYouTubeAuthorization(targetWindow, String(getConfigValue(config, "USER_SESSION_ID") ?? (secondSyncId || firstSyncId) ?? "") || void 0); const loggedIn = getConfigValue(config, "LOGGED_IN") === true; const sessionIndex = getConfigValue(config, "SESSION_INDEX"); const authenticatedAuth = { authorization, sessionIndex, delegatedSessionId }; const playerContexts = getConfigValue(config, "WEB_PLAYER_CONTEXT_CONFIGS"); const pageExperimentFlags = Object.values(playerContexts && typeof playerContexts === "object" ? playerContexts : {}).flatMap((entry) => typeof entry?.serializedExperimentFlags === "string" ? [entry.serializedExperimentFlags] : []); debug.log("Audio downloader. player auth state", { videoId, host: targetWindow.location.hostname, anonymousFirst: true, hasAuthorization: Boolean(authorization), sessionIndex: sessionIndex ?? "none", hasDelegatedSession: Boolean(delegatedSessionId), loggedIn }); let lastError; let emitted = false; let loginRequiredSeen = false; for (const name of [ "web_embedded", "tv_downgraded", "web", "web_creator" ]) { signal.throwIfAborted(); debug.log("Audio downloader. trying player client", { videoId, client: name }); const fetchedConfig = name === "tv_downgraded" ? await fetchTvConfig(targetWindow, signal, videoId) : void 0; const options = { visitorData: fetchedConfig?.visitorData ?? visitorData, signatureTimestamp: fetchedConfig?.signatureTimestamp ?? sts, clientVersion: fetchedConfig?.clientVersion }; const candidateBody = name === "web_embedded" ? body : name === "tv_downgraded" ? buildTvDowngradedPlayerRequest(videoId, options) : name === "web" ? buildWebPlayerRequest(config, videoId, sts) : buildWebCreatorPlayerRequest(videoId, options); const candidateContext = candidateBody.context; if (typeof options.visitorData === "string") candidateContext.client.visitorData = options.visitorData; const postPlayer = (authenticated) => postInnertubePlayer(targetWindow, signal, fetchedConfig?.apiKey ?? apiKey, candidateBody, name === "web_embedded" ? "56" : name === "tv_downgraded" ? "7" : name === "web" ? "1" : "62", String(candidateContext.client.clientVersion ?? clientVersion), authenticated ? authenticatedAuth : {}); const requestPlayer = async (authenticated = false) => { const response = await postPlayer(authenticated); const status = response.playabilityStatus?.status ?? ""; if (status === "LOGIN_REQUIRED") loginRequiredSeen = true; if (!authenticated && authorization && /LOGIN_REQUIRED|AGE_CHECK_REQUIRED|CONTENT_CHECK_REQUIRED/.test(status)) { debug.log("Audio downloader. retrying player with YouTube session", { videoId, client: name, status }); return { response: await postPlayer(true), authenticated: true }; } return { response, authenticated }; }; const getCode = () => fetchPlayerCode(fetchedConfig?.playerUrl); try { const initialPlayer = await requestPlayer(); const playerResponse = initialPlayer.response; const requestAuthenticated = initialPlayer.authenticated; const formats = [...playerResponse.streamingData?.adaptiveFormats ?? [], ...playerResponse.streamingData?.formats ?? []]; if (!formats.length) { const status = playerResponse.playabilityStatus; throw new Error(`Audio downloader. ${name} ${status?.status ?? "failed"}: ${status?.reason ?? status?.messages?.join(" ") ?? "no streaming data"}`); } const format = selectWebEmbeddedAudioFormat(formats, sourceLanguage); const fetchedFlags = fetchedConfig?.experimentFlags; const poTokenBinding = selectGvsPoTokenBinding(videoId, { loggedIn: requestAuthenticated, dataSyncId: playerResponse.responseContext?.mainAppWebResponseContext?.datasyncId || dataSyncId || fetchedConfig?.dataSyncId, visitorData: candidateContext.client.visitorData ?? visitorData, experimentFlags: fetchedFlags?.length ? fetchedFlags : pageExperimentFlags }); let poToken; const authorizeUrl = async (streamUrl) => { const url = new URL(streamUrl); if (!url.searchParams.has("pot") && poTokenBinding) { poToken ??= mintPagePoToken(targetWindow, poTokenBinding.value, signal); const token = await poToken; if (token) url.searchParams.set("pot", token); } return url.toString(); }; for await (const solvedUrl of resolveWebEmbeddedFormatUrl(targetWindow, format, getCode, signal)) try { const streamUrl = await authorizeUrl(solvedUrl); const contentLength = Number(format.contentLength) || await probeContentLength(targetWindow, streamUrl, signal); const refreshUrl = async () => { const response = (await requestPlayer(requestAuthenticated)).response; const refreshed = [...response.streamingData?.adaptiveFormats ?? [], ...response.streamingData?.formats ?? []].find((entry) => entry.itag === format.itag && entry.mimeType === format.mimeType && Number(entry.contentLength) === contentLength && entry.lastModified === format.lastModified); if (!refreshed) throw new Error("Audio downloader. Refreshed audio format changed"); for await (const url of resolveWebEmbeddedFormatUrl(targetWindow, refreshed, getCode, signal)) return await authorizeUrl(url); throw new Error("Audio downloader. Refreshed audio URL unavailable"); }; for await (const chunk of downloadMediaRanges(targetWindow, streamUrl, contentLength, signal, refreshUrl)) { emitted = true; yield chunk; } return; } catch (error) { signal.throwIfAborted(); if (emitted) throw error; lastError = error; } } catch (error) { signal.throwIfAborted(); if (error instanceof Error && error.message === "YOUTUBE_SIGN_IN_SUGGESTED") throw error; if (emitted) throw error; debug.log("Audio downloader. player client format failed", { videoId, client: name, error: error instanceof Error ? error.message : String(error) }); lastError = error; } } if (loginRequiredSeen && !authorization) throw new Error("YOUTUBE_SIGN_IN_SUGGESTED", { cause: lastError }); const fallbackError = lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Audio downloader. no playable audio formats"); if (/LOGIN_REQUIRED|UNPLAYABLE/.test(fallbackError.message)) throw new Error(`${fallbackError.message}. Anonymous playback and the available signed-in YouTube session were both unable to provide a playable audio stream`, { cause: fallbackError }); throw fallbackError; } //#endregion //#region src/audioDownloader/strategies/youtubeSabrPlayer.ts function readInitialPlayerResponseFromDocument(targetWindow) { let scripts = []; try { scripts = [...getTopPageWindow(targetWindow).document.querySelectorAll("script:not([src])")]; } catch { return; } const markers = [ "ytInitialPlayerResponse =", "ytInitialPlayerResponse=", "window[\"ytInitialPlayerResponse\"] =", "window['ytInitialPlayerResponse'] =" ]; for (const script of scripts) { const source = script.textContent ?? ""; if (!source.includes("ytInitialPlayerResponse")) continue; for (const marker of markers) { let cursor = 0; while (cursor < source.length) { const markerIndex = source.indexOf(marker, cursor); if (markerIndex < 0) break; let start = markerIndex + marker.length; while (start < source.length && /\s/.test(source[start] ?? "")) start++; if (source[start] !== "{") { cursor = start + 1; continue; } const end = findJsonValueEnd(source, start); if (end < 0) break; try { const value = JSON.parse(source.slice(start, end)); if (value && typeof value === "object") return value; } catch {} cursor = end; } } } } function getNativePlayerResponse(targetWindow, videoId) { const candidates = []; try { const moviePlayer = getTopPageWindow(targetWindow).document.querySelector("#movie_player"); if (moviePlayer && typeof moviePlayer.getPlayerResponse === "function") { const response = moviePlayer.getPlayerResponse(); if (response && typeof response === "object") candidates.push(response); } } catch {} try { if (targetWindow.ytInitialPlayerResponse) candidates.push(targetWindow.ytInitialPlayerResponse); } catch {} const documentResponse = readInitialPlayerResponseFromDocument(targetWindow); if (documentResponse) candidates.push(documentResponse); for (const value of candidates) { const responseVideoId = value.videoDetails?.videoId; if (responseVideoId && responseVideoId !== videoId) continue; if (value.streamingData?.serverAbrStreamingUrl && value.playerConfig?.mediaCommonConfig?.mediaUstreamerRequestConfig?.videoPlaybackUstreamerConfig && value.streamingData?.adaptiveFormats?.length) return value; } } function selectEconomyAudioFormat(formats) { const audioOnly = formats.filter((format) => typeof format.itag === "number" && format.mimeType?.includes("audio/") && !format.mimeType?.includes("video/")); const nonDrc = audioOnly.filter((format) => !isDrcAudioFormat(format)); const candidates = nonDrc.length > 0 ? nonDrc : audioOnly; const contentLength = (format) => { const value = Number(format.contentLength); return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY; }; const bitrate = (format) => { const value = Number(format.averageBitrate ?? format.bitrate); return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY; }; return [...candidates].sort((a, b) => { const sizeDiff = contentLength(a) - contentLength(b); if (Number.isFinite(sizeDiff) && sizeDiff !== 0) return sizeDiff; const bitrateDiff = bitrate(a) - bitrate(b); if (Number.isFinite(bitrateDiff) && bitrateDiff !== 0) return bitrateDiff; return (a.itag ?? Number.MAX_SAFE_INTEGER) - (b.itag ?? Number.MAX_SAFE_INTEGER); })[0]; } var N_PATTERN = /^[A-Za-z0-9_-]{4,}$/; async function resolveSabrStreamingUrl(targetWindow, rawUrl, config, signal) { signal.throwIfAborted(); const url = new URL(rawUrl); const n = url.searchParams.get("n") ?? void 0; if (!n) return url.toString(); const playerUrl = getPlayerUrl(config); if (!playerUrl) throw new Error("Audio downloader. YouTube player URL is unavailable for SABR n solve"); const response = await targetWindow.fetch(playerUrl, { signal }); if (!response.ok) throw new Error(`Audio downloader. YouTube player request failed (${response.status})`); const code = await response.text(); signal.throwIfAborted(); const solvedN = validPageValue(solveYouTubeChallenges(targetWindow, code, void 0, n).n, n, N_PATTERN); if (!solvedN) throw new Error("Audio downloader. SABR n challenge solve invalid"); url.searchParams.set("n", solvedN); return url.toString(); } function buildContentPlaybackContext(signatureTimestamp) { const context = { html5Preference: "HTML5_PREF_WANTS" }; const timestamp = Number(signatureTimestamp); if (Number.isFinite(timestamp) && timestamp > 0) context.signatureTimestamp = timestamp; return context; } function buildSabrPlayerRequest(config, videoId, extractedSignatureTimestamp) { const rawContext = getConfigValue(config, "INNERTUBE_CONTEXT"); if (!rawContext || typeof rawContext !== "object") throw new Error("Audio downloader. web client context is unavailable"); const context = JSON.parse(JSON.stringify(rawContext)); context.client ??= {}; const client = context.client; client.clientName = "WEB"; client.clientVersion = getConfigValue(config, "INNERTUBE_CLIENT_VERSION") ?? client.clientVersion; client.originalUrl = `https://www.youtube.com/watch?v=${videoId}`; delete context.thirdParty; return { context, videoId, playbackContext: { contentPlaybackContext: buildContentPlaybackContext(extractedSignatureTimestamp ?? getConfigValue(config, "STS")) }, contentCheckOk: true, racyCheckOk: true }; } //#endregion //#region src/audioDownloader/strategies/youtubeSabrSupport.ts function toSabrFormat(format) { const itag = Number(format.itag); const bitrate = Number(format.bitrate ?? format.averageBitrate); const approxDurationMs = Number(format.approxDurationMs); const lastModified = String(format.lastModified ?? ""); if (!(itag > 0) || !(bitrate > 0) || !(approxDurationMs > 0) || !lastModified) return; const language = getYoutubeAudioFormatLanguage(format) || void 0; return { itag, lastModified, xtags: format.xtags, width: format.width, height: format.height, contentLength: Number(format.contentLength) || void 0, audioTrackId: format.audioTrackId ?? format.audioTrack?.id, mimeType: format.mimeType, isDrc: isDrcAudioFormat(format), quality: format.quality, qualityLabel: format.qualityLabel, averageBitrate: Number(format.averageBitrate) || void 0, bitrate, audioQuality: format.audioQuality, approxDurationMs, language, isOriginal: format.audioTrack?.audioIsDefault === true }; } function encodeProtoVarint(value) { const out = []; let current = Math.max(0, Math.floor(value)); do { let byte = current % 128; current = Math.floor(current / 128); if (current > 0) byte |= 128; out.push(byte); } while (current > 0); return new Uint8Array(out); } function concatBytes(parts) { const size = parts.reduce((sum, part) => sum + part.byteLength, 0); const out = new Uint8Array(size); let offset = 0; for (const part of parts) { out.set(part, offset); offset += part.byteLength; } return out; } function readProtoVarintAt(bytes, offset) { let value = 0; let shift = 0; let cursor = offset; while (cursor < bytes.length && shift <= 49) { const byte = bytes[cursor++]; value += (byte & 127) * 2 ** shift; if ((byte & 128) === 0) return { value, next: cursor }; shift += 7; } throw new Error("invalid protobuf varint"); } function splitTopLevelProto(bytes) { const out = []; let offset = 0; while (offset < bytes.length) { const start = offset; const tag = readProtoVarintAt(bytes, offset); offset = tag.next; const field = Math.floor(tag.value / 8); const wire = tag.value & 7; if (wire === 0) offset = readProtoVarintAt(bytes, offset).next; else if (wire === 1) offset += 8; else if (wire === 2) { const length = readProtoVarintAt(bytes, offset); offset = length.next + length.value; } else if (wire === 5) offset += 4; else throw new Error(`unsupported protobuf wire ${wire}`); if (offset > bytes.length) throw new Error("truncated protobuf field"); out.push({ field, start, end: offset }); } return out; } /** * SABR-only: set ClientAbrState.audioTrackId (nested field 69 inside * VideoPlaybackAbrRequest field 1). We keep SabrStream's generated ABR state * authoritative and only add/replace the concrete YouTube audio track id. * * Native requests observed on multi-audio videos carry values such as * "ar.10", "de-DE.10" and "en-US.4" in this field. */ function setGeneratedSabrAudioTrackId(generated, trackId) { const topLevel = splitTopLevelProto(generated); const encodedTrackId = new TextEncoder().encode(trackId); const trackField = concatBytes([ encodeProtoVarint(554), encodeProtoVarint(encodedTrackId.byteLength), encodedTrackId ]); const chunks = []; let replacedClientAbrState = false; for (const part of topLevel) { if (part.field !== 1 || replacedClientAbrState) { chunks.push(generated.slice(part.start, part.end)); continue; } const length = readProtoVarintAt(generated, readProtoVarintAt(generated, part.start).next); const payloadStart = length.next; const payloadEnd = payloadStart + length.value; if (payloadEnd > part.end) throw new Error("truncated ClientAbrState"); const payload = generated.slice(payloadStart, payloadEnd); const payloadChunks = splitTopLevelProto(payload).filter((field) => field.field !== 69).map((field) => payload.slice(field.start, field.end)); payloadChunks.push(trackField); const patchedPayload = concatBytes(payloadChunks); chunks.push(concatBytes([ encodeProtoVarint(10), encodeProtoVarint(patchedPayload.byteLength), patchedPayload ])); replacedClientAbrState = true; } if (!replacedClientAbrState) throw new Error("SABR ClientAbrState field is unavailable"); return concatBytes(chunks); } function getYouTubeRuntimeAudioTracks(targetWindow) { try { const value = getTopPageWindow(targetWindow).document.querySelector("#movie_player")?.getAvailableAudioTracks?.(); return Array.isArray(value) ? value.filter((track) => Boolean(track && typeof track === "object")) : []; } catch { return []; } } function getRuntimeAudioTrackId(track) { let methodId; try { methodId = track.wM?.getId?.(); } catch {} const id = typeof methodId === "string" ? methodId : track.wM?.id; return typeof id === "string" && /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})?\.\d{1,3}$/.test(id) ? id : void 0; } function collectYouTubeSabrTrackDiscoveries(targetWindow) { const pageWindow = getTopPageWindow(targetWindow); const roots = []; const addRoot = (value, source) => { if (value != null) roots.push({ value, source }); }; try { addRoot(pageWindow.ytInitialPlayerResponse, "ytInitialPlayerResponse"); } catch {} try { const player = pageWindow.document.querySelector("#movie_player"); if (player) { for (const getter of [ "getPlayerResponse", "getAudioTrack", "getAudioTrackList", "getAvailableAudioTracks" ]) try { const fn = player[getter]; if (typeof fn === "function") addRoot(fn.call(player), `player.${getter}()`); } catch {} if (typeof player.getOption === "function") for (const [namespace, key] of [["audio", "tracklist"], ["audio", "track"]]) try { addRoot(player.getOption(namespace, key), `player.getOption(${namespace},${key})`); } catch {} for (const key of [ "playerData", "playerResponse", "audioTracks", "tracklist", "config" ]) try { addRoot(player[key], `player.${key}`); } catch {} } } catch {} const trackIdPattern = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})?\.\d{1,3}$/; const found = /* @__PURE__ */ new Map(); const seen = /* @__PURE__ */ new WeakSet(); let visited = 0; const walk = (value, depth, path, source) => { if (value == null || depth > 9 || visited > 3e4) return; if (typeof value === "string") { if (trackIdPattern.test(value) && /audio/i.test(`${source}:${path}`)) found.set(`${value}|${source}|${path}`, { trackId: value, source: `${source}:${path}` }); return; } if (typeof value !== "object") return; if (seen.has(value)) return; seen.add(value); visited += 1; const record = value; for (const key of [ "trackId", "audioTrackId", "id" ]) { const id = record[key]; if (typeof id === "string" && trackIdPattern.test(id) && (/audio/i.test(`${source}:${path}`) || key === "audioTrackId")) found.set(`${id}|${source}|${path}`, { trackId: id, raw: value, source: `${source}:${path}` }); } const nestedAudioTrack = record.audioTrack; if (nestedAudioTrack && typeof nestedAudioTrack === "object") { const nestedId = nestedAudioTrack.id; if (typeof nestedId === "string" && trackIdPattern.test(nestedId)) found.set(`${nestedId}|${source}|${path}.audioTrack`, { trackId: nestedId, raw: nestedAudioTrack, source: `${source}:${path}.audioTrack` }); } let entries; try { entries = Array.isArray(value) ? value.slice(0, 400).map((child, index) => [String(index), child]) : Object.entries(record).slice(0, 400); } catch { return; } for (const [key, child] of entries) { if (typeof child === "function") continue; walk(child, depth + 1, path ? `${path}.${key}` : key, source); } }; for (const root of roots) walk(root.value, 0, "", root.source); return [...found.values()]; } function collectYouTubeSabrTrackIds(targetWindow) { const runtimeIds = getYouTubeRuntimeAudioTracks(targetWindow).map(getRuntimeAudioTrackId).filter((id) => Boolean(id)); if (runtimeIds.length > 0) return [...new Set(runtimeIds)].sort(); return [...new Set(collectYouTubeSabrTrackDiscoveries(targetWindow).map((entry) => entry.trackId))].sort(); } function switchYouTubeSabrAudioTrack(targetWindow, trackId) { const pageWindow = getTopPageWindow(targetWindow); try { const player = pageWindow.document.querySelector("#movie_player"); if (!player || typeof player.setAudioTrack !== "function") return false; const exact = getYouTubeRuntimeAudioTracks(targetWindow).find((track) => getRuntimeAudioTrackId(track) === trackId); if (!exact) return false; const current = player.getAudioTrack?.(); if (current && getRuntimeAudioTrackId(current) === trackId) return true; player.setAudioTrack(exact); return true; } catch { return false; } } var VOT_SUPPORTED_SABR_SOURCE_LANGUAGES = new Set([ "ru", "en", "zh", "ko", "fr", "it", "es", "de", "ja" ].map((language) => normalizeAudioLanguageTag(language)).filter(Boolean)); function sabrTrackLanguage(trackId) { return normalizeAudioLanguageTag(trackId.replace(/\.\d+$/, "")); } function isVotSupportedSabrTrack(trackId) { const language = sabrTrackLanguage(trackId); if (!language) return false; if (VOT_SUPPORTED_SABR_SOURCE_LANGUAGES.has(language)) return true; const base = language.split("-")[0]; return [...VOT_SUPPORTED_SABR_SOURCE_LANGUAGES].some((supported) => supported.split("-")[0] === base); } var WEB_ABR_RESOLVED_AUDIO_LANGUAGES = /* @__PURE__ */ new Map(); function resolveSabrAudioTrackId(targetWindow, requestedLanguage) { const requested = normalizeAudioLanguageTag(requestedLanguage); const availableTrackIds = collectYouTubeSabrTrackIds(targetWindow); const supportedAlternativeTrackIds = availableTrackIds.filter(isVotSupportedSabrTrack); if (!requested || requested === "auto") return { availableTrackIds, supportedAlternativeTrackIds, usedSupportedAlternative: false }; if (availableTrackIds.length === 0) return { availableTrackIds, supportedAlternativeTrackIds, usedSupportedAlternative: false }; const exact = availableTrackIds.find((trackId) => sabrTrackLanguage(trackId) === requested); if (exact) return { trackId: exact, availableTrackIds, supportedAlternativeTrackIds, usedSupportedAlternative: false }; const requestedBase = requested.split("-")[0]; const baseMatches = availableTrackIds.filter((trackId) => sabrTrackLanguage(trackId).split("-")[0] === requestedBase); if (baseMatches.length === 1) return { trackId: baseMatches[0], availableTrackIds, supportedAlternativeTrackIds, usedSupportedAlternative: false }; if (baseMatches.length > 1) throw new Error(`Audio downloader. SABR audio language ${requestedLanguage} is ambiguous (matches: ${baseMatches.join(", ")})`); return { availableTrackIds, supportedAlternativeTrackIds, usedSupportedAlternative: false }; } async function waitForSabrAudioTrackId(targetWindow, signal, requestedLanguage, timeoutMs = 2e3) { const requested = normalizeAudioLanguageTag(requestedLanguage); if (!requested || requested === "auto") return resolveSabrAudioTrackId(targetWindow, requestedLanguage); const startedAt = Date.now(); let lastAvailable = []; while (Date.now() - startedAt < timeoutMs) { signal.throwIfAborted(); const resolved = resolveSabrAudioTrackId(targetWindow, requestedLanguage); lastAvailable = resolved.availableTrackIds; if (resolved.trackId) return resolved; await createAbortableDelay(250, signal); } if (lastAvailable.length === 0) return { availableTrackIds: [], supportedAlternativeTrackIds: [], usedSupportedAlternative: false }; throw new Error(`Audio downloader. SABR could not resolve requested audio language ${requestedLanguage} to a YouTube trackId within ${timeoutMs}ms (available: ${lastAvailable.join(", ")})`); } var VOT_SABR_INSTANCE_CONTEXT = /* @__PURE__ */ new WeakMap(); //#endregion //#region src/audioDownloader/strategies/youtubeSabr.ts function readProtoVarint(bytes, start) { let value = 0; let shift = 0; let offset = start; while (offset < bytes.length && shift <= 35) { const byte = bytes[offset++]; value += (byte & 127) * 2 ** shift; if ((byte & 128) === 0) return { value, next: offset }; shift += 7; } throw new Error("invalid protobuf varint"); } function getGeneratedSabrAudioTrackId(bytes) { try { let offset = 0; while (offset < bytes.length) { const tag = readProtoVarint(bytes, offset); offset = tag.next; const field = Math.floor(tag.value / 8); const wire = tag.value & 7; if (wire !== 2) { if (wire === 0) offset = readProtoVarint(bytes, offset).next; else if (wire === 1) offset += 8; else if (wire === 5) offset += 4; else return void 0; continue; } const length = readProtoVarint(bytes, offset); const start = length.next; const end = start + length.value; if (end > bytes.length) return void 0; if (field === 1) { let inner = start; while (inner < end) { const innerTag = readProtoVarint(bytes, inner); inner = innerTag.next; const innerField = Math.floor(innerTag.value / 8); const innerWire = innerTag.value & 7; if (innerWire === 2) { const innerLength = readProtoVarint(bytes, inner); const valueStart = innerLength.next; const valueEnd = valueStart + innerLength.value; if (valueEnd > end) return void 0; if (innerField === 69) { const trackId = new TextDecoder().decode(bytes.slice(valueStart, valueEnd)); return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})?\.\d{1,3}$/.test(trackId) ? trackId : void 0; } inner = valueEnd; } else if (innerWire === 0) inner = readProtoVarint(bytes, inner).next; else if (innerWire === 1) inner += 8; else if (innerWire === 5) inner += 4; else return void 0; } } offset = end; } } catch {} } async function* trySabrAudioChunks(targetWindow, videoId, signal, sourceLanguage) { debug.log("[VOT][SABR][PRIMARY] entered trySabrAudioChunks", { videoId }); WEB_ABR_RESOLVED_AUDIO_LANGUAGES.delete(videoId); const config = await resolveYtcfg(targetWindow, signal); const apiKey = getConfigValue(config, "INNERTUBE_API_KEY"); if (typeof apiKey !== "string") throw new Error("Audio downloader. SABR config is unavailable"); const body = buildSabrPlayerRequest(config, videoId, Number(getConfigValue(config, "STS"))); const context = body.context; const clientVersion = String(context.client.clientVersion ?? ""); const visitorData = context.client.visitorData ?? getConfigValue(config, "VISITOR_DATA"); if (typeof visitorData === "string") context.client.visitorData = visitorData; const dataSyncId = getConfigValue(config, "DATASYNC_ID"); const [firstSyncId, secondSyncId] = typeof dataSyncId === "string" ? dataSyncId.split("||") : []; const authorization = await getYouTubeAuthorization(targetWindow, String(getConfigValue(config, "USER_SESSION_ID") ?? (secondSyncId || firstSyncId) ?? "") || void 0); const auth = authorization ? { authorization, sessionIndex: getConfigValue(config, "SESSION_INDEX"), delegatedSessionId: getConfigValue(config, "DELEGATED_SESSION_ID") ?? (secondSyncId ? firstSyncId : void 0) } : {}; let player = await postInnertubePlayer(targetWindow, signal, apiKey, body, "1", clientVersion, {}); if (authorization && /LOGIN_REQUIRED|AGE_CHECK_REQUIRED|CONTENT_CHECK_REQUIRED/.test(player.playabilityStatus?.status ?? "")) player = await postInnertubePlayer(targetWindow, signal, apiKey, body, "1", clientVersion, auth); const nativePlayer = getNativePlayerResponse(targetWindow, videoId); if (nativePlayer) player = nativePlayer; const playerServerAbrStreamingUrl = player.streamingData?.serverAbrStreamingUrl; const requestedSabrLanguage = normalizeAudioLanguageTag(sourceLanguage); const resolvedSabrTrack = await waitForSabrAudioTrackId(targetWindow, signal, sourceLanguage, 2e3); const sabrAvailableTrackIds = resolvedSabrTrack.availableTrackIds; if (resolvedSabrTrack.trackId) switchYouTubeSabrAudioTrack(targetWindow, resolvedSabrTrack.trackId); const nativeSabrAudioTrackId = void 0; let sabrAudioTrackId = resolvedSabrTrack.trackId; let sabrTrackSelection; if (resolvedSabrTrack.usedSupportedAlternative) { new Set(resolvedSabrTrack.supportedAlternativeTrackIds); sabrTrackSelection = "supported-alternative"; } else if (sabrAudioTrackId) sabrTrackSelection = "ui-language"; else if (sabrAvailableTrackIds.length === 0) { sabrAudioTrackId = nativeSabrAudioTrackId; sabrTrackSelection = requestedSabrLanguage && requestedSabrLanguage !== "auto" ? "native-only" : "native-auto"; } else throw new Error(`Audio downloader. SABR found no VOT-supported audio track (requested: ${sourceLanguage ?? "auto"}, available: ${sabrAvailableTrackIds.join(", ")})`); const actualSabrLanguage = sabrAudioTrackId ? sabrTrackLanguage(sabrAudioTrackId) : requestedSabrLanguage || void 0; WEB_ABR_RESOLVED_AUDIO_LANGUAGES.set(videoId, { videoId, requestedLanguage: requestedSabrLanguage || void 0, actualLanguage: actualSabrLanguage, trackId: sabrAudioTrackId, selection: sabrTrackSelection }); const serverAbrStreamingUrl = playerServerAbrStreamingUrl ? await resolveSabrStreamingUrl(targetWindow, playerServerAbrStreamingUrl, config, signal) : void 0; const videoPlaybackUstreamerConfig = player.playerConfig?.mediaCommonConfig?.mediaUstreamerRequestConfig?.videoPlaybackUstreamerConfig; if (!serverAbrStreamingUrl || !videoPlaybackUstreamerConfig) throw new Error("Audio downloader. SABR metadata is unavailable in player response"); const rawFormats = [...player.streamingData?.adaptiveFormats ?? [], ...player.streamingData?.formats ?? []]; const audioCandidates = rawFormats.filter((format) => format.mimeType?.includes("audio/") && !format.mimeType?.includes("video/")); const normalizedSelectedTrackId = sabrAudioTrackId?.toLowerCase(); const requestedTrackLanguage = normalizedSelectedTrackId ? normalizeAudioLanguageTag(normalizedSelectedTrackId.split(".")[0]) : normalizeAudioLanguageTag(sourceLanguage); const formatTrackId = (format) => format.audioTrackId ?? format.audioTrack?.id; const exactTrackAudioCandidates = normalizedSelectedTrackId ? audioCandidates.filter((format) => formatTrackId(format)?.toLowerCase() === normalizedSelectedTrackId) : []; const languageTrackAudioCandidates = exactTrackAudioCandidates.length === 0 && requestedTrackLanguage ? audioCandidates.filter((format) => audioLanguageMatches(getYoutubeAudioFormatLanguage(format), requestedTrackLanguage)) : []; audioCandidates.some((format) => Boolean(formatTrackId(format) || normalizeAudioLanguageTag(getYoutubeAudioFormatLanguage(format)))); const nativeBoundUnscopedAudioCandidates = (exactTrackAudioCandidates.length === 0 && languageTrackAudioCandidates.length, []); const selectedTrackAudioCandidates = exactTrackAudioCandidates.length > 0 ? exactTrackAudioCandidates : languageTrackAudioCandidates.length > 0 ? languageTrackAudioCandidates : nativeBoundUnscopedAudioCandidates.length > 0 ? nativeBoundUnscopedAudioCandidates : sabrAvailableTrackIds.length <= 1 ? audioCandidates : []; if (selectedTrackAudioCandidates.length === 0) throw new Error(`Audio downloader. SABR has no concrete formats for selected track ${sabrAudioTrackId ?? "native/default"}; refusing array[0] fallback`); const selected = selectEconomyAudioFormat(selectedTrackAudioCandidates); if (!selected?.itag) throw new Error("Audio downloader. SABR audio format is unavailable"); const selectedSabr = toSabrFormat(selected); if (!selectedSabr) throw new Error("Audio downloader. SABR selected format metadata is incomplete"); const selectedTrackAudioObjects = new Set(selectedTrackAudioCandidates); const sabrFormatsForStream = rawFormats.flatMap((format) => { if (format.mimeType?.includes("audio/") && !format.mimeType?.includes("video/") && !selectedTrackAudioObjects.has(format)) return []; const converted = toSabrFormat(format); return converted ? [converted] : []; }); const nativeClient = getConfigValue(config, "INNERTUBE_CONTEXT")?.client ?? {}; const pageWindow = getTopPageWindow(targetWindow); const clientInfo = { clientName: Number(getConfigValue(config, "INNERTUBE_CONTEXT_CLIENT_NAME")) || 1, clientVersion, ...typeof nativeClient.hl === "string" && nativeClient.hl ? { acceptLanguage: nativeClient.hl } : {}, ...typeof nativeClient.deviceMake === "string" && nativeClient.deviceMake ? { deviceMake: nativeClient.deviceMake } : {}, ...typeof nativeClient.deviceModel === "string" && nativeClient.deviceModel ? { deviceModel: nativeClient.deviceModel } : {}, ...typeof nativeClient.osName === "string" && nativeClient.osName ? { osName: nativeClient.osName } : {}, ...typeof nativeClient.osVersion === "string" && nativeClient.osVersion ? { osVersion: nativeClient.osVersion } : {} }; const playerContexts = getConfigValue(config, "WEB_PLAYER_CONTEXT_CONFIGS"); const pageExperimentFlags = Object.values(playerContexts && typeof playerContexts === "object" ? playerContexts : {}).flatMap((entry) => typeof entry?.serializedExperimentFlags === "string" ? [entry.serializedExperimentFlags] : []); const sabrPoTokenBinding = selectGvsPoTokenBinding(videoId, { loggedIn: Boolean(authorization), dataSyncId: player.responseContext?.mainAppWebResponseContext?.datasyncId ?? dataSyncId, visitorData, experimentFlags: pageExperimentFlags }); const sabrPoToken = sabrPoTokenBinding ? await mintPagePoToken(targetWindow, sabrPoTokenBinding.value, signal) : void 0; const readSabrVarint = (bytes, start) => { let value = 0n; let shift = 0n; for (let offset = start; offset < bytes.length && offset < start + 10; offset++) { const byte = bytes[offset]; value |= BigInt(byte & 127) << shift; if ((byte & 128) === 0) return { value, next: offset + 1 }; shift += 7n; } }; const inspectSabrProto = (bytes) => { const fields = []; let offset = 0; let parseError; while (offset < bytes.length && fields.length < 512) { const tag = readSabrVarint(bytes, offset); if (!tag) { parseError = `invalid tag at ${offset}`; break; } offset = tag.next; const field = Number(tag.value >> 3n); const wireType = Number(tag.value & 7n); if (!field) { parseError = `field 0 at ${offset}`; break; } if (wireType === 0) { const item = readSabrVarint(bytes, offset); if (!item) { parseError = `invalid varint field ${field}`; break; } offset = item.next; fields.push({ field, wireType, value: item.value.toString() }); } else if (wireType === 1) { if (offset + 8 > bytes.length) { parseError = `truncated fixed64 field ${field}`; break; } fields.push({ field, wireType, length: 8 }); offset += 8; } else if (wireType === 2) { const length = readSabrVarint(bytes, offset); if (!length) { parseError = `invalid length field ${field}`; break; } offset = length.next; const size = Number(length.value); if (!Number.isSafeInteger(size) || size < 0 || offset + size > bytes.length) { parseError = `truncated bytes field ${field} length ${length.value}`; break; } fields.push({ field, wireType, length: size }); offset += size; } else if (wireType === 5) { if (offset + 4 > bytes.length) { parseError = `truncated fixed32 field ${field}`; break; } fields.push({ field, wireType, length: 4 }); offset += 4; } else { parseError = `unsupported wire type ${wireType} field ${field}`; break; } } const counts = {}; const lengths = {}; for (const item of fields) { const key = String(item.field); counts[key] = (counts[key] ?? 0) + 1; if (item.length !== void 0) { lengths[key] ??= []; lengths[key].push(item.length); } } return { byteLength: bytes.byteLength, fieldFingerprint: fields.map((item) => `${item.field}:${item.wireType}:${item.length ?? item.value ?? ""}`).join("|"), fieldCounts: counts, fieldLengths: lengths, targetFields: Object.fromEntries([ 1, 2, 3, 5, 16, 19 ].map((field) => [String(field), { count: counts[String(field)] ?? 0, lengths: lengths[String(field)] ?? [] }])), parseError: parseError ?? null, parsedBytes: offset }; }; let sabrRequestIndex = 0; let consecutivePostBootstrapRequestsWithoutBufferedRanges = 0; const fetchRealm = targetWindow; const sabrDiagnosticFetch = async (input, init) => { const index = ++sabrRequestIndex; const RequestCtor = fetchRealm.Request; const inputIsRequest = typeof RequestCtor !== "undefined" && input instanceof RequestCtor; const requestInput = inputIsRequest ? input : void 0; const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const url = new URL(requestUrl, targetWindow.location.href); const isSabrRequest = url.hostname.endsWith("googlevideo.com") && url.pathname === "/videoplayback" && url.searchParams.get("sabr") === "1"; if (isSabrRequest) {} const method = init?.method ?? requestInput?.method ?? "GET"; const body = init?.body ?? null; const headers = new fetchRealm.Headers(init?.headers ?? requestInput?.headers); if (isSabrRequest) { headers.delete("content-type"); headers.delete("Content-Type"); } let transportBody = body; const BlobCtor = fetchRealm.Blob; if (isSabrRequest && typeof BlobCtor !== "undefined" && body instanceof BlobCtor) transportBody = await body.arrayBuffer(); const headerEntries = Object.fromEntries(headers.entries()); if (body !== null) { let bytes = null; if (typeof body === "string") bytes = new TextEncoder().encode(body); else if (body instanceof ArrayBuffer) bytes = new Uint8Array(body); else if (ArrayBuffer.isView(body)) bytes = new Uint8Array(body.buffer, body.byteOffset, body.byteLength); else if (typeof BlobCtor !== "undefined" && body instanceof BlobCtor) {} if (bytes) { const protobuf = inspectSabrProto(bytes); const encodedAudioTrackId = getGeneratedSabrAudioTrackId(bytes); if (index <= 3 || encodedAudioTrackId !== sabrAudioTrackId) {} if (index > 1) { consecutivePostBootstrapRequestsWithoutBufferedRanges = (protobuf.targetFields?.["3"]?.count ?? 0) > 0 ? 0 : consecutivePostBootstrapRequestsWithoutBufferedRanges + 1; if (consecutivePostBootstrapRequestsWithoutBufferedRanges >= 8) throw new Error("SABR state did not produce buffered ranges after bootstrap"); } } } const fetchWithGmFallback = async () => { try { const nativeInit = { ...init ?? {}, headers, body: transportBody, credentials: "include", mode: "cors", cache: "no-store", redirect: "follow" }; if (!inputIsRequest) return await targetWindow.fetch(url.toString(), nativeInit); if (!RequestCtor) throw new Error("Audio downloader. SABR Request constructor is unavailable"); const rewrittenRequest = new RequestCtor(url.toString(), { method, headers, body: transportBody ?? void 0, credentials: "include", mode: "cors", cache: "no-store", redirect: "follow", signal: init?.signal ?? requestInput?.signal }); return await targetWindow.fetch(rewrittenRequest); } catch (nativeError) { const gmGlobal = globalThis; const callbackGm = gmGlobal.GM_xmlhttpRequest; const promiseGm = gmGlobal.GM?.xmlHttpRequest ?? gmGlobal.GM?.xmlhttpRequest; const gm = typeof callbackGm === "function" ? callbackGm : typeof promiseGm === "function" ? promiseGm.bind(gmGlobal.GM) : void 0; if (typeof gm !== "function") throw nativeError; let gmBody; if (typeof transportBody === "string") gmBody = transportBody; else if (transportBody instanceof ArrayBuffer) gmBody = transportBody.slice(0); else if (ArrayBuffer.isView(transportBody)) gmBody = transportBody.buffer.slice(transportBody.byteOffset, transportBody.byteOffset + transportBody.byteLength); else if (typeof BlobCtor !== "undefined" && transportBody instanceof BlobCtor) gmBody = await transportBody.arrayBuffer(); else if (transportBody !== null || requestInput?.body) throw nativeError; return await new Promise((resolve, reject) => { let settled = false; let requestHandle; const signal = init?.signal ?? requestInput?.signal; const cleanup = () => signal?.removeEventListener("abort", onAbort); const finishReject = (error) => { if (settled) return; settled = true; cleanup(); reject(error); }; const onAbort = () => { try { requestHandle?.abort?.(); } catch {} finishReject(new DOMException("The operation was aborted.", "AbortError")); }; if (signal?.aborted) { onAbort(); return; } signal?.addEventListener("abort", onAbort, { once: true }); const parseHeaders = (raw) => { const result = new fetchRealm.Headers(); for (const line of (raw ?? "").split(/\r?\n/)) { const colon = line.indexOf(":"); if (colon > 0) try { result.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim()); } catch {} } return result; }; const finishResolve = (gmResponse) => { if (settled) return; settled = true; cleanup(); const responseHeaders = parseHeaders(gmResponse.responseHeaders); resolve(new fetchRealm.Response(gmResponse.response ?? /* @__PURE__ */ new ArrayBuffer(0), { status: gmResponse.status || 200, statusText: gmResponse.statusText ?? "", headers: responseHeaders })); }; const details = { method, url: url.toString(), headers: headerEntries, data: gmBody, responseType: "arraybuffer", anonymous: false, onload: finishResolve, onerror: (gmError) => finishReject(gmError instanceof Error ? gmError : /* @__PURE__ */ new TypeError("GM_xmlhttpRequest SABR request failed")), ontimeout: () => finishReject(/* @__PURE__ */ new TypeError("GM_xmlhttpRequest SABR request timed out")), onabort: () => finishReject(new DOMException("The operation was aborted.", "AbortError")) }; try { requestHandle = gm(details); if (requestHandle && typeof requestHandle.then === "function") requestHandle.then(finishResolve, finishReject); } catch (gmError) { finishReject(gmError); } }); } }; const response = await fetchWithGmFallback(); if (response.body) { const foreignReader = response.body.getReader(); const localBody = new ReadableStream({ async pull(controller) { try { const { value, done } = await foreignReader.read(); if (done) { controller.close(); return; } if (!value) return; const source = ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value); const localChunk = new Uint8Array(source.byteLength); localChunk.set(source); controller.enqueue(localChunk); } catch (error) { controller.error(error); } }, cancel(reason) { return foreignReader.cancel(reason); } }); return new Response(localBody, { status: response.status, statusText: response.statusText, headers: new Headers(response.headers) }); } return response; }; const sabrPrototype = SabrStream.prototype; if (typeof sabrPrototype.buildRequestBody === "function" && !sabrPrototype.__votNativeAbrStatePatched) { const originalBuildRequestBody = sabrPrototype.buildRequestBody; sabrPrototype.buildRequestBody = function(abrState, selectedAudioFormat, selectedVideoFormat) { const context = VOT_SABR_INSTANCE_CONTEXT.get(this); const generated = originalBuildRequestBody.call(this, abrState, selectedAudioFormat, selectedVideoFormat); if (!context) return generated; context.buildIndex += 1; try { let selfBuilt = generated; if (context.sabrAudioTrackId) selfBuilt = setGeneratedSabrAudioTrackId(selfBuilt, context.sabrAudioTrackId); return selfBuilt; } catch { return generated; } }; sabrPrototype.__votNativeAbrStatePatched = true; } let stream; stream = new SabrStream({ fetch: sabrDiagnosticFetch, poToken: sabrPoToken, serverAbrStreamingUrl, videoPlaybackUstreamerConfig, clientInfo, formats: sabrFormatsForStream }); VOT_SABR_INSTANCE_CONTEXT.set(stream, { videoId, sabrAudioTrackId, buildIndex: 0, pageWindow }); debug.log("[VOT][SABR][PURE_ONLY] SabrStream created", { videoId, sabrAudioTrackId, bootstrap: "playerResponse+SabrStream-generated-protobuf", nativeSabrCapture: "disabled", nativeBodyUsed: false, nativeUrlUsed: false, nativeCpnUsed: false, nativeRnUsed: false, serverAbrSource: "playerResponse", onlySabrStrategy: "PURE_SELF_BUILT" }); const abort = () => stream.abort(); signal.addEventListener("abort", abort, { once: true }); try { const started = await stream.start({ audioFormat: selectedSabr, videoFormat: (formats) => formats.filter((format) => format.mimeType?.includes("video/")).sort((a, b) => a.bitrate - b.bitrate)[0], enabledTrackTypes: 1, maxRetries: 3, stallDetectionMs: 2e4 }); debug.log("[VOT][SABR][PURE_ONLY] SabrStream started", { videoId, sabrAudioTrackId }); const audioReader = started.audioStream.getReader(); const pending = []; let pendingSize = 0; let readyChunk = null; let total = 0; const configuredMinChunkSize = Number(config_default$1.minChunkSize); const minChunkSize = Number.isFinite(configuredMinChunkSize) && configuredMinChunkSize > 0 ? Math.floor(configuredMinChunkSize) : 5295308; console.log("[VOT][SABR] chunk config", { configuredMinChunkSize: config_default$1.minChunkSize, minChunkSize, usedFallback: minChunkSize !== configuredMinChunkSize }); try { while (true) { signal.throwIfAborted(); let result; const readTimeoutMs = 25e3; let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(/* @__PURE__ */ new Error(`Audio downloader. SABR audio stream stalled for ${readTimeoutMs}ms`)); }, readTimeoutMs); }); try { result = await Promise.race([audioReader.read(), timeout]); } finally { if (timeoutId !== void 0) clearTimeout(timeoutId); } const { value, done } = result; if (done) break; if (!value?.byteLength) continue; const copy = new Uint8Array(value.byteLength); copy.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)); total += copy.byteLength; let copyOffset = 0; while (copyOffset < copy.byteLength) { const remaining = minChunkSize - pendingSize; const take = Math.min(remaining, copy.byteLength - copyOffset); pending.push(copy.subarray(copyOffset, copyOffset + take)); pendingSize += take; copyOffset += take; if (pendingSize >= minChunkSize) { const nextChunk = concatBuffers(pending); pending.length = 0; pendingSize = 0; if (readyChunk) { console.log("[VOT][SABR] yielding chunk", { size: readyChunk.byteLength, isLastChunk: false, total, pendingSize, aborted: signal.aborted }); yield { buffer: readyChunk, isLastChunk: false }; console.log("[VOT][SABR] resumed after yield", { total, aborted: signal.aborted }); } readyChunk = nextChunk; } } } if (pendingSize > 0) { const tail = concatBuffers(pending); pending.length = 0; pendingSize = 0; if (readyChunk) { console.log("[VOT][SABR] yielding chunk", { size: readyChunk.byteLength, isLastChunk: false, total, pendingSize, aborted: signal.aborted }); yield { buffer: readyChunk, isLastChunk: false }; console.log("[VOT][SABR] resumed after yield", { total, aborted: signal.aborted }); } readyChunk = tail; } if (total < 1 || !readyChunk) throw new Error("Audio downloader. SABR returned empty audio"); const expectedContentLength = Number(selected.contentLength); if (Number.isFinite(expectedContentLength) && expectedContentLength > 0) { if (expectedContentLength - total > Math.max(262144, expectedContentLength * .02)) throw new Error(`Audio downloader. SABR premature EOF (${total}/${expectedContentLength} bytes)`); } console.log("[VOT][SABR] yielding final chunk", { size: readyChunk.byteLength, isLastChunk: true, total, aborted: signal.aborted }); yield { buffer: readyChunk, isLastChunk: true }; console.log("[VOT][SABR] resumed after final yield", { total, aborted: signal.aborted }); } catch (error) { console.error("[VOT][SABR] audio reader failed", { error, total, pendingSize, readyChunkSize: readyChunk?.byteLength ?? 0, aborted: signal.aborted, reason: signal.reason }); throw error; } finally { console.log("[VOT][SABR] releasing audio reader", { total, pendingSize, readyChunkSize: readyChunk?.byteLength ?? 0, aborted: signal.aborted, reason: signal.reason }); try { audioReader.releaseLock(); } catch {} } } finally { signal.removeEventListener("abort", abort); stream.abort(); } } //#endregion //#region src/audioDownloader/strategies/youtubeSabrStrategy.ts var SABR_MAX_ATTEMPTS = 2; var SABR_RETRY_DELAY_MS = 750; var SABR_DOWNLOAD_QUEUE = /* @__PURE__ */ new Map(); function isDeterministicSabrError(error) { const message = error instanceof Error ? error.message : String(error); return message.includes("SABR could not resolve requested audio language") || message.includes("SABR found no VOT-supported audio track") || message.includes("SABR audio language") && message.includes("is ambiguous") || message.includes("refusing array[0] fallback"); } async function* getSabrAudioChunksImpl(targetWindow, videoId, signal, sourceLanguage) { let lastError; for (let attempt = 1; attempt <= SABR_MAX_ATTEMPTS; attempt++) { const chunks = []; let bytes = 0; try { for await (const chunk of trySabrAudioChunks(targetWindow, videoId, signal, sourceLanguage)) { const buffer = chunk.buffer.slice(); chunks.push({ buffer, isLastChunk: chunk.isLastChunk }); bytes += buffer.byteLength; } if (!chunks.length) throw new Error("Audio downloader. SABR completed without audio chunks"); debug.log("Audio downloader. SABR completed", { videoId, attempt, chunks: chunks.length, bytes }); yield* chunks; return; } catch (error) { signal.throwIfAborted(); lastError = error; debug.log("Audio downloader. SABR attempt failed", { videoId, attempt, bufferedChunks: chunks.length, bufferedBytes: bytes, error: error instanceof Error ? error.message : String(error) }); if (attempt < SABR_MAX_ATTEMPTS && !isDeterministicSabrError(error)) { await createAbortableDelay(SABR_RETRY_DELAY_MS, signal); continue; } break; } } throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "Audio downloader. SABR failed")); } async function* getSabrAudioChunks(targetWindow, videoId, signal, sourceLanguage) { const queueKey = String(videoId); const previous = SABR_DOWNLOAD_QUEUE.get(queueKey) ?? Promise.resolve(); const hadPrevious = SABR_DOWNLOAD_QUEUE.has(queueKey); let releaseCurrent; const current = new Promise((resolve) => { releaseCurrent = resolve; }); SABR_DOWNLOAD_QUEUE.set(queueKey, current); debug.log("Audio downloader. SABR queued", { videoId, hasPrevious: hadPrevious }); try { await previous; signal.throwIfAborted(); yield* getSabrAudioChunksImpl(targetWindow, videoId, signal, sourceLanguage); } finally { releaseCurrent?.(); if (SABR_DOWNLOAD_QUEUE.get(queueKey) === current) SABR_DOWNLOAD_QUEUE.delete(queueKey); debug.log("Audio downloader. SABR queue released", { videoId }); } } //#endregion //#region src/audioDownloader/strategies/youtubeWebAbr.ts var WEB_ABR_DOWNLOAD_QUEUE = /* @__PURE__ */ new Map(); /** Pure WebABR strategy. SABR is implemented separately. */ async function* getWebAbrAudioChunks(targetWindow, videoId, signal, sourceLanguage) { const queueKey = String(videoId); const previous = WEB_ABR_DOWNLOAD_QUEUE.get(queueKey) ?? Promise.resolve(); const hadPrevious = WEB_ABR_DOWNLOAD_QUEUE.has(queueKey); let releaseCurrent; const current = new Promise((resolve) => { releaseCurrent = resolve; }); WEB_ABR_DOWNLOAD_QUEUE.set(queueKey, current); debug.log("Audio downloader. WebABR queued", { videoId, hasPrevious: hadPrevious }); try { await previous; signal.throwIfAborted(); yield* getWebAbrAudioChunks$1(targetWindow, videoId, signal, sourceLanguage); } finally { releaseCurrent?.(); if (WEB_ABR_DOWNLOAD_QUEUE.get(queueKey) === current) WEB_ABR_DOWNLOAD_QUEUE.delete(queueKey); debug.log("Audio downloader. WebABR queue released", { videoId }); } } //#endregion //#region src/audioDownloader/strategies/mseProxyHandler.ts var MESSAGE_TYPE = "get-audio-chunks-by-mse-in-main-world"; var READY_MESSAGE_TYPE = "vot-mse-proxy-ready"; var IFRAME_HASH = "ya_iframe"; var BOOT_KEY = "__VOT_MSE_PROXY_HANDLER__"; var STORE_KEY = "__VOT_MSE_CAPTURE_STORE__"; var topSessions = /* @__PURE__ */ new Map(); function getVideoId(message) { if (!message.payload || typeof message.payload !== "object") return; const videoId = message.payload.pureVideoId; return typeof videoId === "string" ? videoId : void 0; } function getAudioDownloadType(message) { if (!message.payload || typeof message.payload !== "object") return; const audioDownloadType = message.payload.audioDownloadType; return isAudioBridgeStrategy(audioDownloadType) ? audioDownloadType : void 0; } function getSourceLanguage(message) { if (!message.payload || typeof message.payload !== "object") return; const sourceLanguage = message.payload.sourceLanguage; return typeof sourceLanguage === "string" && sourceLanguage ? sourceLanguage : void 0; } async function getEncryptedEmbedConfig(targetWindow, videoId) { if (!/(?:^|\.)youtube\.com$/.test(targetWindow.location.hostname)) return; const bytes = new Uint8Array(2 + videoId.length); bytes[0] = 10; bytes[1] = videoId.length; for (let index = 0; index < videoId.length; index++) bytes[index + 2] = videoId.charCodeAt(index); try { const match = (await (await targetWindow.fetch("https://www.youtube.com/youtubei/v1/share/get_share_panel", { method: "POST", body: JSON.stringify({ context: { client: { clientName: "WEB", clientVersion: "2.20251006.01.00" } }, serializedSharedEntity: encodeURIComponent(targetWindow.btoa(String.fromCharCode(...bytes))) }) })).text()).match(/"encryptedEmbedConfig"\s*:\s*("[^"]+")/); return match ? `{"enc":${match[1]}}` : void 0; } catch { return; } } function waitFor(getValue, timeoutMs, label, signal) { return new Promise((resolve, reject) => { const cleanup = () => { clearInterval(interval); clearTimeout(timeout); signal.removeEventListener("abort", onAbort); }; const onAbort = () => { cleanup(); reject(signal.reason); }; const interval = setInterval(() => { try { const value = getValue(); if (value) { cleanup(); resolve(value); } } catch (error) { cleanup(); reject(error); } }, 100); const timeout = setTimeout(() => { cleanup(); reject(/* @__PURE__ */ new Error(`Audio downloader. ${label} timed out`)); }, timeoutMs); signal.addEventListener("abort", onAbort, { once: true }); if (signal.aborted) onAbort(); }); } var CapturedMediaSource = class { mediaSource; createdAt = performance.now(); queuedEvents = []; listeners = /* @__PURE__ */ new Set(); constructor(mediaSource) { this.mediaSource = mediaSource; const addSourceBuffer = mediaSource.addSourceBuffer; mediaSource.addSourceBuffer = new Proxy(addSourceBuffer, { apply: (target, thisArg, args) => { const sourceBuffer = Reflect.apply(target, thisArg, args); if (args[0].includes("audio/webm")) this.capture(sourceBuffer); return sourceBuffer; } }); const endOfStream = mediaSource.endOfStream; mediaSource.endOfStream = new Proxy(endOfStream, { apply: (target, thisArg, args) => { const result = Reflect.apply(target, thisArg, args); this.emit({ type: "end" }); return result; } }); mediaSource.addEventListener("sourceclose", () => this.emit({ type: "close" })); } get isReady() { return this.mediaSource.readyState === "open"; } listen(listener) { this.listeners.add(listener); try { for (const event of this.queuedEvents.splice(0)) listener(event); } catch (error) { this.listeners.delete(listener); throw error; } return () => this.listeners.delete(listener); } capture(sourceBuffer) { const appendBuffer = sourceBuffer.appendBuffer; sourceBuffer.appendBuffer = new Proxy(appendBuffer, { apply: (target, thisArg, args) => { const input = args[0]; const view = ArrayBuffer.isView(input) ? new Uint8Array(input.buffer, input.byteOffset, input.byteLength) : new Uint8Array(input); const copy = new Uint8Array(view); const result = Reflect.apply(target, thisArg, args); this.emit({ type: "append", buffer: copy, sourceBuffer }); return result; } }); } emit(event) { if (this.listeners.size === 0) this.queuedEvents.push(event); for (const listener of this.listeners) listener(event); } }; var MseCaptureStore = class { captures = []; listeners = /* @__PURE__ */ new Set(); add(mediaSource) { const capture = new CapturedMediaSource(mediaSource); this.captures.push(capture); for (const listener of this.listeners) listener(capture); } async pick(signal) { try { return await waitFor(() => { const capture = this.captures.at(-1); return capture?.isReady && performance.now() - capture.createdAt >= 4e3 ? capture : null; }, 1e4, "MSE capture wait", signal); } catch (error) { signal.throwIfAborted(); if (isAbortError(error)) throw error; const newest = this.captures.at(-1); throw new Error(`Audio downloader. MSE capture wait timed out (captures: ${this.captures.length}, newestReady: ${newest?.isReady ?? "none"})`, { cause: error }); } } onCapture(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } }; function installMediaSourceProxy(targetWindow) { if (targetWindow[STORE_KEY]) return targetWindow[STORE_KEY]; const store = new MseCaptureStore(); const key = targetWindow.ManagedMediaSource ? "ManagedMediaSource" : "MediaSource"; const MediaSourceConstructor = targetWindow[key]; if (!MediaSourceConstructor) throw new Error("MediaSource is not available"); class ProxiedMediaSource extends MediaSourceConstructor { constructor() { super(); store.add(this); } } targetWindow[key] = ProxiedMediaSource; targetWindow[STORE_KEY] = store; return store; } async function getPlayer(targetWindow, signal) { return await waitFor(() => { const player = targetWindow.document.querySelector("#movie_player"); return player && typeof player.playVideo === "function" && typeof player.mute === "function" && typeof player.seekTo === "function" ? player : null; }, 3e4, "MSE player wait", signal); } function createAudioChunkStream(targetWindow, videoId, signal, onProgress) { let cleanup = () => {}; let finished = false; const cancellation = new AbortController(); signal = AbortSignal.any([signal, cancellation.signal]); return new ReadableStream({ async start(controller) { let stopMse = () => {}; const fail = (error) => { if (finished) return; finished = true; cleanup(); controller.error(error); }; const onAbort = () => fail(signal.reason); cleanup = () => { stopMse(); signal.removeEventListener("abort", onAbort); }; signal.addEventListener("abort", onAbort, { once: true }); const onMseError = (error) => { if (finished) return; stopMse(); fail(signal.aborted ? signal.reason : error); }; try { signal.throwIfAborted(); debug.log("Audio downloader. MSE iframe stream started", { videoId }); const player = await getPlayer(targetWindow, signal); signal.throwIfAborted(); debug.log("Audio downloader. MSE player found", { videoId }); try { player.loadVideoById?.(videoId); } catch {} player.mute(); player.playVideo(); const getPlayerState = () => { try { return player.getPlayerState?.() ?? null; } catch { return null; } }; const listVideos = () => [...targetWindow.document.querySelectorAll("video")]; let readyVideo; let playReject = null; try { readyVideo = await waitFor(() => { const videos = listVideos(); const state = getPlayerState(); if (videos.length > 0 && (state === 5 || state === 2 || state === -1)) { try { if (state === -1) player.loadVideoById?.(videoId); player.playVideo(); } catch {} const element = videos[0]; try { element.muted = true; const attempt = element.play(); if (attempt && typeof attempt.catch === "function") attempt.catch((playError) => { playReject ??= playError instanceof Error ? playError.name : String(playError); }); } catch (playError) { playReject ??= playError instanceof Error ? playError.name : String(playError); } } return videos.find((video) => video.readyState >= 3) ?? (state === 1 && videos.length > 0 ? videos[0] : null); }, 15e3, "MSE media wait", signal); } catch (error) { signal.throwIfAborted(); if (isAbortError(error)) throw error; const videos = listVideos(); const video = videos[0]; const earlyStore = targetWindow[STORE_KEY]; const earlyNewest = earlyStore?.captures.at(-1); throw new Error(`Audio downloader. MSE media wait timed out (videos: ${videos.length}, readyState: ${video?.readyState ?? "none"}, playerState: ${getPlayerState() ?? "unknown"}, paused: ${video?.paused ?? "unknown"}, networkState: ${video?.networkState ?? "none"}, buffered: ${video?.buffered.length ?? "none"}, hasSrc: ${Boolean(video?.currentSrc)}, mediaError: ${video?.error?.code ?? "none"}, playReject: ${playReject ?? "none"}, captures: ${earlyStore?.captures.length ?? "none"}, newestMS: ${earlyNewest?.mediaSource.readyState ?? "none"})`, { cause: error }); } signal.throwIfAborted(); debug.log("Audio downloader. MSE media ready", { videoId, readyState: readyVideo.readyState, playerState: getPlayerState(), playReject }); try { readyVideo.playbackRate = 2; } catch {} const store = installMediaSourceProxy(targetWindow); let capture = await store.pick(signal); signal.throwIfAborted(); debug.log("Audio downloader. MSE capture picked", { videoId, captures: store.captures.length, readyState: capture.mediaSource.readyState }); let removeCaptureListener = () => {}; let pending = []; let pendingSize = 0; let totalSize = 0; let seekTimeout; let lastProgressAt = 0; const enqueuePendingChunk = (isLastChunk) => { const size = pendingSize; controller.enqueue({ buffer: concatBuffers(pending), isLastChunk }); debug.log("Audio downloader. MSE chunk enqueued", { videoId, size, isLastChunk, totalSize }); pending = []; pendingSize = 0; }; const close = () => { if (finished) return; if (totalSize === 0) { debug.error("Audio downloader. MSE empty stream", { videoId }); onMseError(/* @__PURE__ */ new Error("Audio downloader. Empty MSE stream")); } else { debug.log("Audio downloader. MSE stream finished", { videoId, totalSize }); enqueuePendingChunk(true); finished = true; controller.close(); cleanup(); } }; let firstAppendLogged = false; const onCapturedEvent = (event) => { if (finished) return; try { if (event.type === "end") { debug.log("Audio downloader. MSE end of stream", { videoId, totalSize, pendingSize }); close(); return; } if (event.type === "close") { debug.error("Audio downloader. MSE source closed", { videoId, totalSize }); onMseError(/* @__PURE__ */ new Error("Audio downloader. MSE source closed")); return; } if (!firstAppendLogged) { firstAppendLogged = true; debug.log("Audio downloader. MSE first audio append", { videoId, size: event.buffer.byteLength }); } pending.push(event.buffer); pendingSize += event.buffer.byteLength; totalSize += event.buffer.byteLength; if (pendingSize >= config_default$1.minChunkSize) enqueuePendingChunk(false); else if (pendingSize >= config_default$1.minChunkSize / 2 && performance.now() - lastProgressAt >= 3e4) { lastProgressAt = performance.now(); debug.log("Audio downloader. MSE progress ping", { videoId, pendingSize, totalSize }); onProgress?.(); } if (finished) return; const { buffered } = event.sourceBuffer; const bufferedEnd = buffered.length > 0 ? Math.floor(buffered.end(buffered.length - 1)) : 0; clearTimeout(seekTimeout); if (bufferedEnd > 0) seekTimeout = setTimeout(() => { try { player.seekTo(bufferedEnd, true); } catch (error) { onMseError(error); } }, 1e3); } catch (error) { onMseError(error); } }; let stopCapture = () => {}; stopMse = () => { clearTimeout(seekTimeout); stopCapture(); removeCaptureListener(); }; stopCapture = capture.listen(onCapturedEvent); if (finished) { stopCapture(); return; } removeCaptureListener = store.onCapture((nextCapture) => { if (finished) return; try { stopCapture(); capture = nextCapture; stopCapture = capture.listen(onCapturedEvent); if (finished) stopCapture(); } catch (error) { onMseError(error); } }); if (finished) removeCaptureListener(); } catch (error) { debug.error("Audio downloader. MSE iframe stream failed", { videoId, error: error instanceof Error ? error.message : String(error) }); onMseError(error); } }, cancel(reason) { finished = true; cancellation.abort(reason); cleanup(); } }); } function postResponse(target, targetOrigin, message) { target.postMessage(message, targetOrigin || "*"); } async function handleIframeRequest(event, targetWindow) { const message = event.data; const source = event.source; if (!source) return; const controller = new AbortController(); const abort = (abortEvent) => { const data = abortEvent.data; if (abortEvent.source === source && abortEvent.origin === event.origin && data.messageId === message.messageId && data.messageType === MESSAGE_TYPE && data.messageDirection === "request" && data.isAborted) controller.abort(data.payload); }; targetWindow.addEventListener("message", abort); let settled = false; let heartbeat; try { const videoId = getVideoId(message); if (!videoId) throw new Error("Audio downloader. Missing video id"); const audioDownloadType = getAudioDownloadType(message); if (!audioDownloadType) throw new Error("Audio downloader. Unsupported audio download type"); debug.log("Audio downloader. iframe request started", { videoId, messageId: message.messageId, audioDownloadType }); const postProgress = () => { if (settled) return; postResponse(source, event.origin, { ...message, messageDirection: "iframe-response", payload: void 0, isProgress: true }); }; const chunks = audioDownloadType === SABR_STRATEGY ? getSabrAudioChunks(targetWindow, videoId, controller.signal, getSourceLanguage(message)) : audioDownloadType === AudioDownloadType.WEB_ABR ? getWebAbrAudioChunks(targetWindow, videoId, controller.signal, getSourceLanguage(message)) : createAudioChunkStream(targetWindow, videoId, controller.signal, postProgress); if (audioDownloadType === SABR_STRATEGY || audioDownloadType === AudioDownloadType.WEB_ABR) { postProgress(); heartbeat = setInterval(postProgress, 3e4); } for await (const chunk of chunks) { debug.log("Audio downloader. iframe chunk sent", { videoId, messageId: message.messageId, audioDownloadType, size: chunk.buffer.byteLength, isLastChunk: chunk.isLastChunk }); postResponse(source, event.origin, { ...message, messageDirection: "iframe-response", payload: chunk }); } settled = true; debug.log("Audio downloader. iframe stream closed", { videoId, messageId: message.messageId, audioDownloadType }); postResponse(source, event.origin, { ...message, messageDirection: "iframe-response", payload: void 0, isStreamFinished: true }); } catch (error) { settled = true; debug.error("Audio downloader. iframe request failed", { messageId: message.messageId, error: error instanceof Error ? error.message : String(error) }); postResponse(source, event.origin, { ...message, messageDirection: "iframe-response", payload: void 0, error: error instanceof Error ? error.message : String(error), isAborted: controller.signal.aborted || isAbortError(error) }); } finally { clearInterval(heartbeat); targetWindow.removeEventListener("message", abort); } } async function handleTopRequest(event, targetWindow) { const message = event.data; const source = event.source; if (!source) return; if (message.isAborted) { const session = topSessions.get(message.messageId); if (session?.source === source && session.origin === event.origin) { session.iframe.contentWindow?.postMessage(message, "*"); session.cleanup(); } return; } const videoId = getVideoId(message); const audioDownloadType = getAudioDownloadType(message); if (!videoId || !audioDownloadType) { postResponse(source, event.origin, { ...message, messageDirection: "response", error: videoId ? "Audio downloader. Unsupported audio download type" : "Audio downloader. Missing video id" }); return; } debug.log("Audio downloader. top request started", { videoId, messageId: message.messageId, audioDownloadType, host: targetWindow.location.hostname }); if (audioDownloadType === SABR_STRATEGY || audioDownloadType === AudioDownloadType.WEB_ABR) { const controller = new AbortController(); let settled = false; const abort = (abortEvent) => { const data = abortEvent.data; if (abortEvent.source === source && abortEvent.origin === event.origin && data.messageId === message.messageId && data.messageType === MESSAGE_TYPE && data.messageDirection === "request" && data.isAborted) controller.abort(data.payload); }; targetWindow.addEventListener("message", abort); const postProgress = () => { if (settled) return; postResponse(source, event.origin, { ...message, messageDirection: "response", payload: void 0, isProgress: true }); }; const heartbeat = setInterval(postProgress, 3e4); postProgress(); debug.log("Audio downloader. direct strategy request started", { strategy: audioDownloadType, videoId, messageId: message.messageId, host: targetWindow.location.hostname }); try { const chunks = audioDownloadType === SABR_STRATEGY ? getSabrAudioChunks(targetWindow, videoId, controller.signal, getSourceLanguage(message)) : getWebAbrAudioChunks(targetWindow, videoId, controller.signal, getSourceLanguage(message)); for await (const chunk of chunks) { const buffer = new Uint8Array(chunk.buffer.buffer, chunk.buffer.byteOffset, chunk.buffer.byteLength).slice(); const payload = { ...chunk, buffer }; debug.log("Audio downloader. direct chunk received", { videoId, messageId: message.messageId, size: buffer.byteLength, isLastChunk: chunk.isLastChunk }); try { postResponse(source, event.origin, { ...message, messageDirection: "response", payload }); debug.log("Audio downloader. direct chunk posted", { videoId, messageId: message.messageId, size: buffer.byteLength, isLastChunk: chunk.isLastChunk }); } catch (error) { debug.error("Audio downloader. direct chunk post failed", { videoId, messageId: message.messageId, size: buffer.byteLength, error: error instanceof Error ? error.message : String(error) }); throw error; } } settled = true; debug.log("Audio downloader. direct strategy stream finished", { strategy: audioDownloadType, videoId, messageId: message.messageId }); postResponse(source, event.origin, { ...message, messageDirection: "response", payload: void 0, isStreamFinished: true }); } catch (error) { settled = true; debug.error("Audio downloader. direct WebABR/SABR request failed", { videoId, messageId: message.messageId, error: error instanceof Error ? error.message : String(error) }); postResponse(source, event.origin, { ...message, messageDirection: "response", payload: void 0, error: error instanceof Error ? error.message : String(error), isAborted: controller.signal.aborted || isAbortError(error) }); } finally { clearInterval(heartbeat); targetWindow.removeEventListener("message", abort); } return; } const iframe = targetWindow.document.createElement("iframe"); iframe.style.cssText = "position:fixed;right:0;bottom:0;width:2px;height:2px;border:0;padding:0;margin:0;opacity:0;visibility:hidden;pointer-events:none;"; iframe.tabIndex = -1; iframe.setAttribute("aria-hidden", "true"); iframe.id = `vot-mse-proxy-${message.messageId}`; const url = new URL(`/embed/${videoId}`, "https://www.youtube.com"); url.searchParams.set("html5", "1"); url.searchParams.set("autoplay", "0"); url.searchParams.set("mute", "1"); url.hash = IFRAME_HASH; let active = true; let timeout; let onMessage = (_event) => {}; const cleanup = () => { active = false; clearTimeout(timeout); targetWindow.removeEventListener("message", onMessage); if (topSessions.get(message.messageId)?.iframe === iframe) topSessions.delete(message.messageId); iframe.remove(); }; topSessions.set(message.messageId, { iframe, cleanup, source, origin: event.origin }); const embedConfig = await getEncryptedEmbedConfig(targetWindow, videoId); if (!active) return; if (embedConfig) url.searchParams.set("embed_config", embedConfig); let ready = false; onMessage = (responseEvent) => { const response = responseEvent.data; if (responseEvent.source !== iframe.contentWindow) return; if (response.messageType === READY_MESSAGE_TYPE) { if (ready) return; ready = true; clearTimeout(timeout); debug.log("Audio downloader. iframe ready", { videoId, messageId: message.messageId }); iframe.contentWindow?.postMessage(message, "*"); } else if (response.messageId === message.messageId && response.messageType === MESSAGE_TYPE && response.messageDirection === "iframe-response") { postResponse(source, event.origin, { ...response, messageDirection: "response" }); if (response.error || response.isAborted || response.isStreamFinished) queueMicrotask(cleanup); } }; timeout = setTimeout(() => { debug.error("Audio downloader. iframe loading timed out", { videoId, messageId: message.messageId, ready }); postResponse(source, event.origin, { ...message, messageDirection: "response", error: "Audio downloader. iframe loading timed out" }); cleanup(); }, 15e3); targetWindow.addEventListener("message", onMessage); iframe.src = url.toString(); (targetWindow.document.body ?? targetWindow.document.documentElement).appendChild(iframe); } function initMseProxyHandler() { const pageWindow = globalThis; if (pageWindow[BOOT_KEY] || !pageWindow.location || pageWindow.navigator.userAgent.includes("YaBrowser/")) return; pageWindow[BOOT_KEY] = true; const isServiceIframe = pageWindow.self !== pageWindow.top && /(?:youtube(?:-nocookie)?\.com|youtubekids\.com)$/.test(pageWindow.location.hostname) && pageWindow.location.hash.includes(IFRAME_HASH); if (isServiceIframe) installMediaSourceProxy(pageWindow); pageWindow.addEventListener("message", (event) => { const message = event.data; if (message?.messageType !== MESSAGE_TYPE || message.messageDirection !== "request") return; if (!isServiceIframe && event.origin !== pageWindow.location.origin) return; if (isServiceIframe) { if (!message.isAborted) handleIframeRequest(event, pageWindow); } else handleTopRequest(event, pageWindow); }); if (isServiceIframe) pageWindow.parent.postMessage({ messageType: READY_MESSAGE_TYPE, messageDirection: "response" }, "*"); } initMseProxyHandler(); //#endregion //#region src/audioDownloader/strategies/index.ts var strategies = { [SABR_STRATEGY]: (options) => getAudioFromBridge(options, SABR_STRATEGY), [WEB_ABR_STRATEGY]: (options) => getAudioFromBridge(options, WEB_ABR_STRATEGY), [WEB_MSE_PROXY_STRATEGY]: (options) => getAudioFromBridge(options, WEB_MSE_PROXY_STRATEGY) }; //#endregion //#region src/audioDownloader/index.ts async function handleCommonAudioDownloadRequest({ audioDownloader, attemptedStrategy, translationId, videoId, signal, sourceLanguage }) { const audioData = await strategies[attemptedStrategy]({ videoId, signal, sourceLanguage }); if (!audioData) throw new Error("Audio downloader. Can not get audio data"); debug.log("Audio downloader. Url found", { audioDownloadType: attemptedStrategy }); const { getMediaBuffers, fileId } = audioData; throwIfAborted(signal); let index = 0; let pending; let sawTerminal = false; const dispatchChunk = async (chunk, isLastChunk) => { await audioDownloader.onDownloadedPartialAudio.dispatchAsync(translationId, { videoId, fileId, audioData: chunk, version: 1, index, amount: isLastChunk ? index + 1 : 0 }); throwIfAborted(signal); index++; }; for await (const raw of getMediaBuffers()) { throwIfAborted(signal); if (sawTerminal) { if (raw.buffer.byteLength === 0 && raw.isLastChunk) continue; throw new Error("Audio downloader. Malformed audio stream after last chunk"); } if (raw.isLastChunk) { if (raw.buffer.byteLength === 0) { if (!pending) throw new Error("Audio downloader. Empty audio"); await dispatchChunk(pending.buffer, true); pending = void 0; } else { if (pending) await dispatchChunk(pending.buffer, false); pending = raw; } sawTerminal = true; continue; } if (raw.buffer.byteLength === 0) throw new Error("Audio downloader. Empty audio"); if (pending) await dispatchChunk(pending.buffer, false); pending = raw; } if (pending) { if (!pending.isLastChunk) throw new Error("Audio downloader. Stream ended without a last chunk"); await dispatchChunk(pending.buffer, true); } else if (!sawTerminal) throw new Error("Audio downloader. Stream ended without a last chunk"); } var audioDownloadTails = /* @__PURE__ */ new Map(); function waitForPreviousDownload(previous, signal) { return new Promise((resolve, reject) => { if (signal.aborted) { reject(makeAbortError()); return; } const onAbort = () => reject(makeAbortError()); signal.addEventListener("abort", onAbort, { once: true }); const settled = () => { signal.removeEventListener("abort", onAbort); resolve(); }; previous.then(settled, settled); }); } async function acquireAudioDownloadSlot(videoId, signal) { throwIfAborted(signal); const previous = audioDownloadTails.get(videoId) ?? Promise.resolve(); let resolveOwn; const ownDone = new Promise((resolve) => { resolveOwn = resolve; }); const tail = previous.then(() => ownDone, () => ownDone); audioDownloadTails.set(videoId, tail); const cleanup = () => { if (audioDownloadTails.get(videoId) === tail) audioDownloadTails.delete(videoId); }; tail.then(cleanup, cleanup); try { await waitForPreviousDownload(previous, signal); throwIfAborted(signal); } catch (error) { resolveOwn(); throw error; } return resolveOwn; } var AudioDownloader = class { completedAudioCache = null; collectingChunks = /* @__PURE__ */ new Map(); onDownloadedAudio = new EventImpl(); onDownloadedPartialAudio = new EventImpl(); onDownloadAudioError = new EventImpl(); strategy; constructor(strategy = "auto") { this.strategy = strategy; this.onDownloadedPartialAudio.addListener((_translationId, data) => { const chunks = this.collectingChunks.get(data.videoId); if (!chunks) return; chunks[data.index] = data.audioData.slice(); if (data.amount !== void 0 && data.amount > 0 && data.index === data.amount - 1) { this.completedAudioCache = { videoId: data.videoId, fileId: data.fileId, chunks: chunks.slice(0, data.amount), version: data.version }; this.collectingChunks.delete(data.videoId); debug.log("[VOT][AudioDownload] prepared audio cached for retry", { videoId: data.videoId, chunks: data.amount }); } }); debug.log("Audio downloader created", { strategy }); } clearCachedAudio(videoId) { if (this.completedAudioCache?.videoId === videoId) this.completedAudioCache = null; this.collectingChunks.delete(videoId); } async replayCachedAudio(videoId, translationId, signal) { const cached = this.completedAudioCache; if (cached?.videoId !== videoId) return false; debug.log("[VOT][AudioDownload] replaying cached prepared audio", { videoId, chunks: cached.chunks.length }); for (let index = 0; index < cached.chunks.length; index++) { throwIfAborted(signal); await this.onDownloadedPartialAudio.dispatchAsync(translationId, { videoId, fileId: cached.fileId, audioData: cached.chunks[index] ?? /* @__PURE__ */ new Uint8Array(), version: cached.version, index, amount: index === cached.chunks.length - 1 ? cached.chunks.length : 0 }); } return true; } async runAudioDownload(videoId, translationId, signal, sourceLanguage) { if (await this.replayCachedAudio(videoId, translationId, signal)) return; let release; try { release = await acquireAudioDownloadSlot(videoId, signal); } catch (error) { if (signal.aborted || isAbortError(error)) { debug.log("Audio downloader. Audio download aborted", { videoId }); return; } debug.error("Audio downloader. All audio download strategies failed", { videoId }); this.onDownloadAudioError.dispatch(translationId, videoId, false); return; } let collecting; try { if (await this.replayCachedAudio(videoId, translationId, signal)) return; collecting = []; this.collectingChunks.set(videoId, collecting); const attempts = this.strategy === "auto" ? [ SABR_STRATEGY, WEB_ABR_STRATEGY, WEB_MSE_PROXY_STRATEGY ] : [this.strategy]; for (const attemptedStrategy of attempts) try { await handleCommonAudioDownloadRequest({ audioDownloader: this, attemptedStrategy, translationId, videoId, signal, sourceLanguage }); debug.log("Audio downloader. Audio download finished", { videoId, sourceLanguage, audioDownloadType: attemptedStrategy }); return; } catch (error) { if (signal.aborted || isAbortError(error)) { debug.log("Audio downloader. Audio download aborted", { videoId, audioDownloadType: attemptedStrategy }); return; } if (attemptedStrategy === WEB_ABR_STRATEGY && error instanceof Error && error.message.includes("YOUTUBE_SIGN_IN_SUGGESTED")) { this.onDownloadAudioError.dispatch(translationId, videoId, true); return; } debug.error("Audio downloader. Strategy failed", { videoId, audioDownloadType: attemptedStrategy, error: error instanceof Error ? error.message : String(error) }); } debug.error("Audio downloader. All audio download strategies failed", { videoId }); this.onDownloadAudioError.dispatch(translationId, videoId, false); } finally { if (collecting && this.collectingChunks.get(videoId) === collecting) this.collectingChunks.delete(videoId); release(); } } addEventListener(type, listener) { switch (type) { case "downloadedAudio": this.onDownloadedAudio.addListener(listener); break; case "downloadedPartialAudio": this.onDownloadedPartialAudio.addListener(listener); break; case "downloadAudioError": this.onDownloadAudioError.addListener(listener); } return this; } removeEventListener(type, listener) { switch (type) { case "downloadedAudio": this.onDownloadedAudio.removeListener(listener); break; case "downloadedPartialAudio": this.onDownloadedPartialAudio.removeListener(listener); break; case "downloadAudioError": this.onDownloadAudioError.removeListener(listener); } return this; } }; //#endregion //#region src/utils/account.ts function hasAccountToken(account) { return typeof account?.token === "string" && account.token.length > 0; } function isAccountExpired(account, now = Date.now()) { return hasAccountToken(account) && typeof account?.expires === "number" && Number.isFinite(account.expires) && account.expires <= now; } function hasValidAccountToken(account, now = Date.now()) { return hasAccountToken(account) && !isAccountExpired(account, now); } function clearAccountState(owner) { if (owner?.data) owner.data.account = {}; if (owner?.votClient) owner.votClient.provider.apiToken = void 0; } async function deleteAccount(owner) { clearAccountState(owner); await votStorage.delete("account"); } async function deleteExpiredAccount(owner, now = Date.now()) { if (!isAccountExpired(owner?.data?.account, now)) return false; await deleteAccount(owner); return true; } //#endregion //#region src/VOTLocalizedError.ts var VOTLocalizedError = class extends Error { name = "VOTLocalizedError"; /** Original (non-localized) message key. */ unlocalizedMessage; /** Resolved localized message. */ localizedMessage; constructor(message) { super(localizationProvider.getDefault(message)); this.unlocalizedMessage = message; this.localizedMessage = localizationProvider.get(message); } }; //#endregion //#region src/core/authWindow.ts var AUTH_WINDOW_NAME = "votAuthWindow"; var AUTH_WINDOW_WIDTH = 520; var AUTH_WINDOW_HEIGHT = 720; function getViewportMetric(name) { const value = globalThis[name]; return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null; } function getScreenOffset(name) { const value = globalThis[name]; return typeof value === "number" && Number.isFinite(value) ? value : 0; } function getAuthWindowFeatures() { const viewportWidth = getViewportMetric("outerWidth") ?? globalThis.screen?.availWidth ?? AUTH_WINDOW_WIDTH; const viewportHeight = getViewportMetric("outerHeight") ?? globalThis.screen?.availHeight ?? AUTH_WINDOW_HEIGHT; const left = Math.max(0, Math.round(getScreenOffset("screenX") + (viewportWidth - AUTH_WINDOW_WIDTH) / 2)); const top = Math.max(0, Math.round(getScreenOffset("screenY") + (viewportHeight - AUTH_WINDOW_HEIGHT) / 2)); return [ "popup=yes", "resizable=yes", "scrollbars=yes", `width=${AUTH_WINDOW_WIDTH}`, `height=${AUTH_WINDOW_HEIGHT}`, `left=${left}`, `top=${top}` ].join(","); } function openAuthWindow() { globalThis.open(authLoginUrl, AUTH_WINDOW_NAME, getAuthWindowFeatures())?.focus?.(); } //#endregion //#region src/core/translationAuthError.ts function isSessionRequired(value) { return safeNestedGet(value, ["status"]) === VideoTranslationStatus.SESSION_REQUIRED || safeNestedGet(value, ["data", "status"]) === VideoTranslationStatus.SESSION_REQUIRED; } function getTranslationServerErrorMessage(value) { const msg = safeNestedGet(value, ["data", "message"]); return typeof msg === "string" && msg.length > 0 ? msg : void 0; } function getTranslationAuthErrorKind(value, context = {}) { if (!isSessionRequired(value)) return null; return context.hasAccountToken ? "session-expired" : "account-required"; } function isTranslationAuthError(value, context = {}) { return getTranslationAuthErrorKind(value, context) !== null; } //#endregion //#region src/core/translationErrors.ts function notifyTranslationFailureIfNeeded(options) { if (options.aborted) return false; if (!options.translateApiErrorsEnabled || !options.hadAsyncWait) return options.hadAsyncWait; options.notify({ videoId: options.videoId, message: options.error }); return false; } //#endregion //#region src/utils/timeFormatting.ts var MAX_SECS_FRACTION = .66; function formatTranslationEta(secs, getMessage) { let minutes = Math.floor(secs / 60); if (Math.floor(secs % 60) / 60 >= MAX_SECS_FRACTION) minutes += 1; if (minutes >= 60) return getMessage("translationTakeMoreThanHour"); if (minutes <= 1) return getMessage("translationTakeAboutMinute"); const minutesStr = String(minutes); if (minutes !== 11 && minutes % 10 === 1) return getMessage("translationTakeApproximatelyMinute2").replace("{0}", minutesStr); if (![ 12, 13, 14 ].includes(minutes) && [ 2, 3, 4 ].includes(minutes % 10)) return getMessage("translationTakeApproximatelyMinute").replace("{0}", minutesStr); return getMessage("translationTakeApproximatelyMinutes").replace("{0}", minutesStr); } //#endregion //#region src/core/translationEtaCountdown.ts var COUNTDOWN_TICK_MS = 1e3; function normalizeRemainingTime(remainingTimeSeconds) { if (!Number.isFinite(remainingTimeSeconds)) return 0; return Math.max(0, Math.ceil(remainingTimeSeconds)); } function createEtaMessage(remainingSeconds) { if (remainingSeconds <= 0) return new VOTLocalizedError("TranslationDelayed"); return formatTranslationEta(remainingSeconds, (key) => localizationProvider.get(key)); } function getMessageIdentity(message) { return message instanceof VOTLocalizedError ? `${message.name}:${message.unlocalizedMessage}` : message; } /** * Keeps the visible translation ETA moving between polling requests. * * Server responses still remain the source of truth: every call to `sync()` * recalculates the deadline from the latest `remainingTime`. Timer ticks only * render the local wall-clock countdown to avoid stale UI while the next poll is * still waiting. */ var TranslationEtaCountdown = class { updateMessage; deadlineMs = 0; generation = 0; lastMessageIdentity = null; signal; timeoutId; constructor(updateMessage) { this.updateMessage = updateMessage; } async sync(remainingTimeSeconds, signal, options = {}) { this.stop(); const remainingSeconds = normalizeRemainingTime(remainingTimeSeconds); if (remainingSeconds <= 0 || signal.aborted) return; const generation = ++this.generation; this.deadlineMs = Date.now() + remainingSeconds * 1e3; this.signal = signal; await this.tick(generation, Boolean(options.countLongWaitOnFirstRender)); } stop() { this.generation += 1; this.deadlineMs = 0; this.lastMessageIdentity = null; this.signal = void 0; if (this.timeoutId !== void 0) { clearTimeout(this.timeoutId); this.timeoutId = void 0; } } async tick(generation, countLongWait = false) { if (generation !== this.generation) return; const signal = this.signal; if (!signal || signal.aborted) { this.stop(); return; } const remainingSeconds = Math.max(0, Math.ceil((this.deadlineMs - Date.now()) / 1e3)); const message = createEtaMessage(remainingSeconds); const messageIdentity = getMessageIdentity(message); if (messageIdentity !== this.lastMessageIdentity) { this.lastMessageIdentity = messageIdentity; await this.updateMessage(message, signal, { countLongWait }); } if (generation !== this.generation || signal.aborted) return; if (remainingSeconds <= 0) { this.timeoutId = void 0; return; } this.timeoutId = setTimeout(() => { this.tick(generation); }, COUNTDOWN_TICK_MS); } }; //#endregion //#region src/core/translationHandler.ts /** * Historically we used `patch-package` to make `@vot.js/core` throw * `VOTLocalizedError` for a few common failure cases. * * We now keep the dependency unpatched and instead map known error messages * coming from the VOT client to the corresponding localized UI errors. */ function mapVotClientErrorForUi(error, hasProvidedAccountToken = false) { const authErrorKind = getTranslationAuthErrorKind(error, { hasAccountToken: hasProvidedAccountToken }); if (authErrorKind) return new VOTLocalizedError(authErrorKind === "session-expired" ? "VOTYandexTokenExpired" : "VOTAccountRequired"); if (!error || typeof error !== "object") return error; if (safeNestedGet(error, ["name"]) !== "VOTJSError") return error; const message = typeof safeNestedGet(error, ["message"]) === "string" ? safeNestedGet(error, ["message"]) : ""; const serverMessage = safeNestedGet(error, ["data", "message"]); const hasServerMessage = typeof serverMessage === "string" && serverMessage.length > 0; if (message === "Yandex couldn't translate video" && !hasServerMessage) return new VOTLocalizedError("requestTranslationFailed"); if (message === "Failed to request video translation") return new VOTLocalizedError("requestTranslationFailed"); if (message === "Audio link wasn't received" || message === "Audio link wasn't received from VOT response") return new VOTLocalizedError("audioNotReceived"); return error; } function summarizeTranslationResponse(response) { return { status: response.status, translated: response.translated, remainingTime: response.remainingTime, translationId: response.translationId }; } var VOTTranslationHandler = class VOTTranslationHandler { videoHandler; audioDownloader; downloading; downloadSettlers = /* @__PURE__ */ new Set(); etaCountdown; requestedFailAudio = /* @__PURE__ */ new Set(); audioRunSeq = 0; audioRunController = null; audioRunExternalUnlinks = /* @__PURE__ */ new Map(); audioRunTranslationId = null; audioRunVideoId = null; linkAudioRunAbort(externalSignal, controller) { if (externalSignal === NEVER_ABORTED_SIGNAL) return; if (this.audioRunExternalUnlinks.has(externalSignal)) return; if (externalSignal.aborted) { controller.abort(makeAbortError()); return; } const onAbort = () => controller.abort(makeAbortError()); externalSignal.addEventListener("abort", onAbort, { once: true }); this.audioRunExternalUnlinks.set(externalSignal, () => externalSignal.removeEventListener("abort", onAbort)); } startAudioRun(externalSignal, translationId, videoId) { this.audioRunSeq += 1; const runId = this.audioRunSeq; const existing = this.audioRunController; if (existing && this.audioRunVideoId === videoId && !existing.signal.aborted) { this.linkAudioRunAbort(externalSignal, existing); this.audioRunTranslationId = translationId; return { signal: existing.signal, runId }; } if (existing) { existing.abort(makeAbortError("New audio run started")); this.cleanupAudioRun(); } const controller = new AbortController(); this.audioRunController = controller; this.audioRunVideoId = videoId; this.audioRunTranslationId = translationId; this.linkAudioRunAbort(externalSignal, controller); return { signal: controller.signal, runId }; } cleanupAudioRun() { for (const unlink of this.audioRunExternalUnlinks.values()) unlink(); this.audioRunExternalUnlinks.clear(); this.audioRunController = null; this.audioRunTranslationId = null; this.audioRunVideoId = null; } finishAudioRun(runId) { if (runId !== this.audioRunSeq) return; this.cleanupAudioRun(); } uploadResumeState = null; constructor(videoHandler) { this.videoHandler = videoHandler; this.audioDownloader = new AudioDownloader(); this.downloading = false; this.etaCountdown = new TranslationEtaCountdown((message, signal, options) => this.videoHandler.updateTranslationErrorMsg(message, signal, options)); this.audioDownloader.addEventListener("downloadedAudio", this.onDownloadedAudio).addEventListener("downloadedPartialAudio", this.onDownloadedPartialAudio).addEventListener("downloadAudioError", this.onDownloadAudioError); } onDownloadedAudio = async (translationId, data) => { debug.log("downloadedAudio", data); if (!this.downloading) { debug.log("skip downloadedAudio"); return; } if (translationId !== this.audioRunTranslationId) { debug.log("skip stale downloadedAudio", { translationId }); return; } const runId = this.audioRunSeq; const signal = this.audioRunController?.signal ?? NEVER_ABORTED_SIGNAL; const { videoId, fileId, audioData } = data; const videoUrl = this.getCanonicalUrl(videoId); try { await this.retryAudioUpload((timeoutMs) => this.videoHandler.votClient.provider.requestVtransAudio(videoUrl, translationId, { audioFile: audioData, fileId }, void 0, {}, { timeout: timeoutMs }), signal); } catch (error) { if (isAbortError(error) && signal.aborted) return; debug.error("Failed to upload downloaded audio", error); this.finishDownloadFailure(error instanceof Error ? error : /* @__PURE__ */ new Error("Audio downloader failed while uploading full audio"), runId); return; } this.audioDownloader.clearCachedAudio(videoId); this.finishDownloadSuccess(runId); }; onDownloadedPartialAudio = async (translationId, data) => { debug.log("downloadedPartialAudio", data); if (!this.downloading) { debug.log("skip downloadedPartialAudio"); return; } if (translationId !== this.audioRunTranslationId) { debug.log("skip stale downloadedPartialAudio", { translationId }); return; } const runId = this.audioRunSeq; const signal = this.audioRunController?.signal ?? NEVER_ABORTED_SIGNAL; const { audioData, fileId, videoId, amount, version, index } = data; const videoUrl = this.getCanonicalUrl(videoId); const resume = this.uploadResumeState; if (resume?.failed && resume.videoId === videoId && resume.fileId === fileId && index <= resume.lastSuccessfulChunkId) { debug.log("[VOT][AudioUpload] skipping already uploaded chunk", { videoId, fileId, chunkId: index, lastSuccessfulChunkId: resume.lastSuccessfulChunkId }); return; } try { await this.retryAudioUpload((timeoutMs) => this.videoHandler.votClient.provider.requestVtransAudio(videoUrl, translationId, { audioFile: audioData, chunkId: index }, { audioPartsLength: amount ?? 0, fileId, version }, {}, { timeout: timeoutMs }), signal); this.uploadResumeState = { videoId, fileId, lastSuccessfulChunkId: index, failed: false }; } catch (error) { if (isAbortError(error) && signal.aborted) return; debug.error("[VOT][AudioUpload] chunk PUT failed after all retries", { videoId, fileId, chunkId: index, amount, bytes: audioData.byteLength, error }); this.uploadResumeState = { videoId, fileId, lastSuccessfulChunkId: this.uploadResumeState?.videoId === videoId ? this.uploadResumeState.lastSuccessfulChunkId : index - 1, failed: true }; this.finishDownloadFailure(new VOTLocalizedError("VOTRetryTranslation"), runId); return; } if (amount !== void 0 && index === amount - 1) { this.uploadResumeState = null; this.audioDownloader.clearCachedAudio(videoId); this.finishDownloadSuccess(runId); } }; onDownloadAudioError = async (translationId, videoId, signInSuggested = false) => { if (!this.downloading) { debug.log("skip downloadAudioError"); return; } if (translationId !== this.audioRunTranslationId) { debug.log("skip stale downloadAudioError", { translationId }); return; } const runId = this.audioRunSeq; debug.error("[VOT][AudioDownload] failed to download audio from source", { videoId }); const videoUrl = this.getCanonicalUrl(videoId); const shouldUseFallback = this.videoHandler.site.host === "youtube" && Boolean(this.videoHandler.data?.useAudioDownload); if (signInSuggested || !shouldUseFallback) { this.finishDownloadFailure(new VOTLocalizedError(signInSuggested ? "VOTYouTubeSignInSuggested" : "VOTFailedDownloadAudio"), runId); return; } try { if (this.requestedFailAudio.has(videoUrl)) debug.log("fail-audio-js request already sent for this video"); else { debug.log("Sending fail-audio-js request"); await this.videoHandler.votClient.provider.requestVtransFailAudio(videoUrl); await this.videoHandler.votClient.provider.requestVtransAudio(videoUrl, translationId, { audioFile: /* @__PURE__ */ new Uint8Array(0), fileId: `fallback-empty-audio:video-translation:${videoId}` }); this.requestedFailAudio.add(videoUrl); } this.finishDownloadSuccess(runId); } catch (error) { debug.error("fail-audio-js request failed", error); this.finishDownloadFailure(new VOTLocalizedError("VOTFailedDownloadAudio"), runId); } }; finishDownloadSuccess(runId) { if (runId !== void 0 && runId !== this.audioRunSeq) return; this.downloading = false; this.settleDownloadWaiters(); } finishDownloadFailure(error, runId) { if (runId !== void 0 && runId !== this.audioRunSeq) return; this.downloading = false; this.settleDownloadWaiters(error); } getCanonicalUrl(videoId) { return `https://youtu.be/${videoId}`; } static AUDIO_UPLOAD_MAX_RETRIES = 5; static AUDIO_UPLOAD_RETRY_DELAY_MS = 1500; static AUDIO_UPLOAD_TIMEOUTS_MS = [ 15e3, 2e4, 3e4 ]; async retryAudioUpload(fn, signal) { const maxRetries = VOTTranslationHandler.AUDIO_UPLOAD_MAX_RETRIES; const delayMs = VOTTranslationHandler.AUDIO_UPLOAD_RETRY_DELAY_MS; const timeouts = VOTTranslationHandler.AUDIO_UPLOAD_TIMEOUTS_MS; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { throwIfAborted(signal); const timeoutMs = timeouts[Math.min(attempt, timeouts.length - 1)]; try { return await fn(timeoutMs); } catch (error) { if (signal.aborted) throw isAbortError(error) ? error : makeAbortError(); lastError = error; const details = error; debug.error("[VOT][AudioUpload] PUT attempt failed", { attempt: attempt + 1, totalAttempts: maxRetries + 1, status: details?.status, code: details?.code, message: details?.message ?? getErrorMessage(error) }); if (attempt === maxRetries) throw error; debug.log(`[VOT][AudioUpload] retry ${attempt + 1}/${maxRetries} after ${delayMs}ms`); await createAbortableDelay(delayMs, signal); } } throw lastError; } /** * Detector for cases when server rejects the request because * "Lively/Live voices" are unavailable (unsupported language pair). */ isLivelyVoiceUnavailableError(value) { if (isTranslationAuthError(value)) return false; const msg = getErrorMessage(value); return !!msg && msg.toLowerCase().includes("обычная озвучка"); } async scheduleRetry(fn, delayMs, signal) { await createAbortableDelay(delayMs, signal, { onScheduled: (timeoutId) => { this.videoHandler.autoRetry = timeoutId; } }); return await fn(); } static MAX_INITIAL_WAIT_SEC = 180; static LONG_WAIT_MS = 12e4; static RETRY_INTERVAL_MS = 3e4; getRetryDelayMs(retryAttempt, remainingTimeSeconds = 0) { if (retryAttempt > 0) return VOTTranslationHandler.RETRY_INTERVAL_MS; const eta = remainingTimeSeconds ?? 0; if (eta <= 0) return VOTTranslationHandler.RETRY_INTERVAL_MS; if (eta <= VOTTranslationHandler.MAX_INITIAL_WAIT_SEC) return eta * 1e3; return VOTTranslationHandler.LONG_WAIT_MS; } async handleTranslationUiError(uiError) { if (!(uiError instanceof VOTLocalizedError)) return; if (uiError.unlocalizedMessage === "VOTYandexTokenExpired") { await deleteAccount(this.videoHandler); openAuthWindow(); } else if (uiError.unlocalizedMessage === "VOTAccountRequired") openAuthWindow(); } async translateVideoImpl(videoData, requestLang, responseLang, translationHelp = null, shouldSendFailedAudio = false, signal = NEVER_ABORTED_SIGNAL, options = {}) { const { disableLivelyVoice = false, retryAttempt = 0 } = options; clearTimeout(this.videoHandler.autoRetry); this.finishDownloadSuccess(); const requestLangForApi = this.videoHandler.getRequestLangForTranslation(requestLang, responseLang); debug.log("[Translation] translateVideoImpl start", { videoId: videoData.videoId, duration: videoData.duration, requestLang, requestLangForApi, responseLang, retryAttempt, disableLivelyVoice, shouldSendFailedAudio, translationHelpCount: translationHelp?.length ?? 0 }); debug.log(videoData, `Translate video (requestLang: ${requestLang}, requestLangForApi: ${requestLangForApi}, responseLang: ${responseLang})`); let livelyDisabled = disableLivelyVoice; let translationResponse; try { throwIfAborted(signal); const livelyVoiceAllowed = this.videoHandler.isLivelyVoiceAllowed(requestLangForApi, responseLang); const translationAttempt = await this.requestTranslationWithLivelyFallback({ videoData, requestLangForApi, responseLang, translationHelp, shouldSendFailedAudio, livelyDisabled, livelyVoiceAllowed }); livelyDisabled = translationAttempt.livelyDisabled; const useLivelyVoice = translationAttempt.useLivelyVoice; const res = translationAttempt.response; translationResponse = res; if (!res) throw new Error("Failed to get translation response"); if (isTranslationAuthError(res, { hasAccountToken: hasAccountToken(this.videoHandler.data?.account) })) throw mapVotClientErrorForUi(res, hasAccountToken(this.videoHandler.data?.account)); debug.log("[Translation] translateVideoImpl response", { videoId: videoData.videoId, useLivelyVoice, ...summarizeTranslationResponse(res) }); throwIfAborted(signal); if (res.translated && res.remainingTime < 1) { this.etaCountdown.stop(); debug.log("[Translation] translation finished", { videoId: videoData.videoId, useLivelyVoice, ...summarizeTranslationResponse(res) }); return { ...res, usedLivelyVoice: useLivelyVoice }; } const message = res.message ?? localizationProvider.get("translationTakeFewMinutes"); debug.log("[Translation] translation still processing", { videoId: videoData.videoId, useLivelyVoice, ...summarizeTranslationResponse(res), message }); if (res.remainingTime > 0) await this.etaCountdown.sync(res.remainingTime, signal, { countLongWaitOnFirstRender: true }); else { this.etaCountdown.stop(); await this.videoHandler.updateTranslationErrorMsg(message, signal); } if (res.status === VideoTranslationStatus.AUDIO_REQUESTED && this.videoHandler.isYouTubeHosts()) { this.videoHandler.hadAsyncWait = true; debug.log("[Translation] audio download started", { videoId: videoData.videoId, translationId: res.translationId }); this.downloading = true; const { signal: audioSignal, runId } = this.startAudioRun(signal, res.translationId, videoData.videoId); debug.log("[Translation] waiting for audio download completion", { videoId: videoData.videoId, translationId: res.translationId, timeoutMs: STREAM_TIMEOUT_MS }); const audioProducer = this.audioDownloader.runAudioDownload(videoData.videoId, res.translationId, audioSignal, requestLang); try { await Promise.all([this.waitForAudioDownloadCompletion(audioSignal, STREAM_TIMEOUT_MS), audioProducer]); } finally { if (runId === this.audioRunSeq) this.downloading = false; audioProducer.then(() => this.finishAudioRun(runId), () => this.finishAudioRun(runId)); } return await this.translateVideoImpl(videoData, requestLang, responseLang, translationHelp, true, signal, { disableLivelyVoice: livelyDisabled, retryAttempt }); } } catch (err) { if (isAbortError(err)) { this.etaCountdown.stop(); debug.log("[Translation] translation aborted", { videoId: videoData.videoId, retryAttempt }); return null; } this.etaCountdown.stop(); const uiError = mapVotClientErrorForUi(err, hasAccountToken(this.videoHandler.data?.account)); debug.error("[Translation] translation failed", { videoId: videoData.videoId, retryAttempt, error: err, mappedError: uiError }); await this.handleTranslationUiError(uiError); await this.videoHandler.updateTranslationErrorMsg(getTranslationServerErrorMessage(uiError) ?? uiError, signal); this.videoHandler.hadAsyncWait = notifyTranslationFailureIfNeeded({ aborted: Boolean(this.videoHandler.actionsAbortController?.signal?.aborted), translateApiErrorsEnabled: Boolean(this.videoHandler.data?.translateAPIErrors), hadAsyncWait: this.videoHandler.hadAsyncWait, videoId: videoData.videoId, error: err, notify: (params) => this.videoHandler.notifier.translationFailed(params) }); return null; } this.videoHandler.hadAsyncWait = true; const retryDelayMs = this.getRetryDelayMs(retryAttempt, translationResponse?.remainingTime ?? null); debug.log("[Translation] scheduling translation retry", { videoId: videoData.videoId, retryAttempt, retryDelayMs, remainingTime: translationResponse?.remainingTime }); return this.scheduleRetry(() => this.translateVideoImpl(videoData, requestLang, responseLang, translationHelp, shouldSendFailedAudio, signal, { disableLivelyVoice: livelyDisabled, retryAttempt: retryAttempt + 1 }), retryDelayMs, signal); } stopTranslationEtaCountdown() { this.etaCountdown.stop(); } async requestTranslationWithLivelyFallback({ videoData, requestLangForApi, responseLang, translationHelp, shouldSendFailedAudio, livelyDisabled, livelyVoiceAllowed }) { let useLivelyVoice = !livelyDisabled && livelyVoiceAllowed && this.videoHandler.data?.useLivelyVoice !== false; debug.log("[Translation] requesting translation from VOT client", { videoId: videoData.videoId, requestLangForApi, responseLang, shouldSendFailedAudio, livelyDisabled, livelyVoiceAllowed, useLivelyVoice, translationHelpCount: translationHelp?.length ?? 0 }); while (true) { try { debug.log("[Translation] votClient.translateVideo call", { videoId: videoData.videoId, requestLangForApi, responseLang, useLivelyVoice, shouldSendFailedAudio, translationHelpCount: translationHelp?.length ?? 0 }); const response = await this.videoHandler.votClient.translateVideo({ videoData, requestLang: requestLangForApi, responseLang, translationHelp, extraOpts: { useLivelyVoice, videoTitle: this.videoHandler.videoData?.title }, shouldSendFailedAudio }); if (!useLivelyVoice || !this.isLivelyVoiceUnavailableError(response)) { debug.log("[Translation] votClient.translateVideo resolved", { videoId: videoData.videoId, useLivelyVoice, ...summarizeTranslationResponse(response) }); return { response, useLivelyVoice, livelyDisabled }; } debug.warn("[Translation] lively voice unavailable in response", { videoId: videoData.videoId, useLivelyVoice, ...summarizeTranslationResponse(response) }); } catch (err) { if (!useLivelyVoice || !this.isLivelyVoiceUnavailableError(err)) throw err; debug.warn("[Translation] lively voice unavailable in error", { videoId: videoData.videoId, useLivelyVoice, error: err }); } livelyDisabled = true; useLivelyVoice = false; debug.log("[Translation] retrying translation without lively voice", { videoId: videoData.videoId, requestLangForApi, responseLang }); } } waitForAudioDownloadCompletion(signal, timeoutMs) { if (!this.downloading) return Promise.resolve(); const { promise, settle } = createAbortableWaiter(signal, timeoutMs); this.downloadSettlers.add(settle); return promise.finally(() => { this.downloadSettlers.delete(settle); }); } settleDownloadWaiters(error) { if (!this.downloadSettlers.size) return; const settlers = Array.from(this.downloadSettlers); this.downloadSettlers.clear(); for (const settle of settlers) if (error) settle.reject(error); else settle.resolve(); } }; //#endregion //#region src/core/translationOrchestrator.ts var TranslationOrchestrator = class { state = { status: "idle" }; deps; constructor(deps) { this.deps = deps; } get currentState() { return this.state; } setState(next) { this.state = next; debug.log("[TranslationOrchestrator] state", next); } reset() { this.setState({ status: "idle" }); } async runAutoTranslationIfEligible() { if (this.state.status !== "idle") return; if (!(this.deps.isFirstPlay() && this.deps.isAutoTranslateEnabled() && this.deps.getVideoId())) return; if (this.deps.isMobileYouTubeMuted?.()) { debug.log("[TranslationOrchestrator] Mobile YouTube video is muted, deferring auto-translate"); this.setState({ status: "deferred", reason: "muted" }); this.deps.setMuteWatcher?.(() => { debug.log("[TranslationOrchestrator] Video unmuted, running deferred auto-translate"); this.setState({ status: "idle" }); this.runAutoTranslationIfEligible(); }); return; } this.setState({ status: "pending", reason: "auto" }); try { await this.deps.scheduleAutoTranslate(); this.deps.setFirstPlay(false); this.reset(); } catch (err) { this.setState({ status: "error", message: err }); throw err; } } }; //#endregion //#region src/core/lifecycleShared.ts function resetLifecycleTranslation(host, options = {}) { const { requireVideoData = false, clearVideoData = false } = options; if (requireVideoData && !host.videoData) return; if (clearVideoData) host.videoData = void 0; host.stopTranslation(); host.resetSubtitlesWidget(); } function hideLifecycleOverlay(overlayView, options = {}) { const { hideMenu = false } = options; if (overlayView?.votButton?.container) overlayView.votButton.container.hidden = true; if (hideMenu && overlayView?.votMenu) overlayView.votMenu.hidden = true; } function resetAndHideLifecycle(host, overlayView, options = {}) { const { requireVideoData, clearVideoData, hideMenu } = options; resetLifecycleTranslation(host, { requireVideoData, clearVideoData }); hideLifecycleOverlay(overlayView, { hideMenu }); } //#endregion //#region src/core/videoLifecycleController.ts var YOUTUBE_TIMESTAMP_PARAMS = [ "t", "start", "time_continue" ]; function getYouTubeSourceKey(url, hasSrcObject) { const stableUrl = new URL(url); for (const param of YOUTUBE_TIMESTAMP_PARAMS) stableUrl.searchParams.delete(param); return `${stableUrl.origin}${stableUrl.pathname}${stableUrl.search}||${hasSrcObject}`; } var VideoLifecycleController = class { host; lifecycleGeneration = 0; lastSetCanPlaySourceKey = ""; activeSetCanPlaySourceKey = ""; setCanPlayRequested = false; setCanPlayLoopPromise; constructor(host) { this.host = host; } isStale(generation) { return generation !== this.lifecycleGeneration; } resetActions(reason) { if (typeof this.host.resetActionsAbortController === "function") { this.host.resetActionsAbortController(reason); return; } this.host.actionsAbortController?.abort(reason); } invalidateActiveSession(reason) { if (this.lifecycleGeneration === 0) return; this.lifecycleGeneration += 1; this.resetActions(`[VideoLifecycle] ${reason}`); debug.log(`[VideoLifecycle] cancelled active session (active: ${this.lifecycleGeneration})`, { reason }); } startSession(reason) { this.lifecycleGeneration += 1; const sessionId = this.lifecycleGeneration; this.resetActions(`[VideoLifecycle][session:${sessionId}] ${reason}`); debug.log(`[VideoLifecycle][session:${sessionId}] started`, { reason }); return sessionId; } shouldAbortHandleSrcChanged(callId, stage) { if (!this.isStale(callId)) return false; debug.log(`[VideoLifecycle][session:${callId}] handleSrcChanged aborted at ${stage} (active: ${this.lifecycleGeneration})`); return true; } showOverlayButton(overlayView) { overlayView.votButton.container.hidden = false; overlayView.votButton.opacity = 1; this.host.queueOverlayAutoHide?.(); } teardown() { this.setCanPlayRequested = false; this.invalidateActiveSession("teardown"); } getCurrentSourceKey() { const hasSrcObject = this.host.video.srcObject ? "1" : "0"; if (this.host.site.host === "youtube") return getYouTubeSourceKey(new URL(globalThis.location.href), hasSrcObject); const src = this.host.video.currentSrc || this.host.video.src || ""; return `${globalThis.location.href}||${src}||${hasSrcObject}`; } resolveContainer() { const { site, video, container } = this.host; if (!site.selector) return video.parentElement ?? container; const matched = findConnectedContainerBySelector(video, site.selector); if (matched) return matched; if (container.isConnected && containsCrossShadow(container, video)) return container; return video.parentElement ?? container; } async setCanPlay() { this.setCanPlayRequested = true; if (this.setCanPlayLoopPromise !== void 0) { const incomingSourceKey = this.getCurrentSourceKey(); if (this.activeSetCanPlaySourceKey && incomingSourceKey !== this.activeSetCanPlaySourceKey) this.invalidateActiveSession("setCanPlay source changed while previous trigger is running"); else debug.log("[VideoLifecycle] setCanPlay deduplicated for same source", { sourceKey: incomingSourceKey }); return await this.setCanPlayLoopPromise; } const loopPromise = (async () => { while (this.setCanPlayRequested) { this.setCanPlayRequested = false; await this.runSetCanPlayOnce(); } })(); this.setCanPlayLoopPromise = loopPromise; try { await loopPromise; } finally { if (this.setCanPlayLoopPromise === loopPromise) this.setCanPlayLoopPromise = void 0; } } async runSetCanPlayOnce() { const sourceKey = this.getCurrentSourceKey(); if (this.host.videoData?.videoId && sourceKey === this.lastSetCanPlaySourceKey) { debug.log("[VideoLifecycle] setCanPlay deduplicated for same source", { sourceKey }); return; } let nextVideoData; try { nextVideoData = await this.host.getVideoData(); } catch (err) { debug.log(`[VideoLifecycle] getVideoData failed for source ${sourceKey}`, err); this.host.videoData = void 0; hideLifecycleOverlay(this.host.uiManager.votOverlayView, { hideMenu: true }); return; } if (this.getCurrentSourceKey() !== sourceKey) { debug.log("[VideoLifecycle] discarded stale getVideoData result after source change", { sourceKey }); return; } this.host.videoData = nextVideoData; this.activeSetCanPlaySourceKey = sourceKey; const currentId = this.startSession(`setCanPlay (source: ${sourceKey})`); debug.log(`[VideoLifecycle][session:${currentId}] setCanPlay started`, { sourceKey }); try { await this.handleSrcChanged(currentId, sourceKey); if (this.isStale(currentId)) { debug.log(`[VideoLifecycle][session:${currentId}] setCanPlay aborted after src change (active: ${this.lifecycleGeneration})`); return; } const autoSubtitlesPromise = this.runAutoSubtitlesIfEnabled(currentId); await this.host.translationOrchestrator.runAutoTranslationIfEligible(); if (this.isStale(currentId)) { debug.log(`[VideoLifecycle][session:${currentId}] auto-translation result ignored (stale session)`); return; } await autoSubtitlesPromise; if (this.isStale(currentId)) { debug.log(`[VideoLifecycle][session:${currentId}] auto-subtitles result ignored (stale session)`); return; } debug.log(`[VideoLifecycle][session:${currentId}] setCanPlay finished`); } finally { if (this.activeSetCanPlaySourceKey === sourceKey) this.activeSetCanPlaySourceKey = ""; } } async runAutoSubtitlesIfEnabled(sessionId) { if (!this.host.data.autoSubtitles || !this.host.videoData?.videoId) return; try { await this.host.enableSubtitlesForCurrentLangPair(); } catch (err) { debug.log(`[VideoLifecycle][session:${sessionId}] auto-subtitles failed`, err); } } async handleSrcChanged(callId, expectedSourceKey) { const sessionId = typeof callId === "number" ? callId : this.startSession("manual handleSrcChanged"); const sourceKey = typeof expectedSourceKey === "string" && expectedSourceKey.length > 0 ? expectedSourceKey : this.getCurrentSourceKey(); if (this.shouldAbortHandleSrcChanged(sessionId, "before start")) return; debug.log(`[VideoLifecycle][session:${sessionId}] src changed`, { sourceKey }); this.host.firstPlay = true; const overlayView = this.host.uiManager.votOverlayView; resetAndHideLifecycle(this.host, overlayView, { requireVideoData: true }); if (!this.host.video.src && !this.host.video.currentSrc && !this.host.video.srcObject) hideLifecycleOverlay(overlayView, { hideMenu: true }); const nextContainer = this.resolveContainer(); if (nextContainer !== this.host.container) this.host.container = nextContainer; if (this.shouldAbortHandleSrcChanged(sessionId, "before getVideoData")) return; this.showOverlayButton(overlayView); if (this.shouldAbortHandleSrcChanged(sessionId, "after getVideoData")) return; if (!this.host.videoData?.videoId) { debug.log(`[VideoLifecycle][session:${sessionId}] No videoId resolved, hiding overlay`); hideLifecycleOverlay(overlayView, { hideMenu: true }); return; } const subtitleLanguage = this.host.getPreferredSubtitlesLanguage(this.host.videoData.detectedLanguage, this.host.videoData.responseLanguage); if (subtitleLanguage) { const cacheKey = this.host.getSubtitlesCacheKey(this.host.videoData.videoId, this.host.videoData.detectedLanguage, subtitleLanguage); const cachedSubtitles = this.host.cacheManager.getSubtitles(cacheKey); this.host.subtitles = cachedSubtitles ?? []; this.host.subtitlesCacheKey = cachedSubtitles === void 0 ? null : cacheKey; } else { this.host.subtitles = []; this.host.subtitlesCacheKey = null; } await this.host.updateSubtitlesLangSelect(); if (this.shouldAbortHandleSrcChanged(sessionId, "after subtitles update")) return; this.host.translateToLang = this.host.data.responseLanguage ?? "ru"; this.host.setSelectMenuValues(this.host.videoData.detectedLanguage, this.host.videoData.responseLanguage); this.showOverlayButton(overlayView); this.lastSetCanPlaySourceKey = sourceKey; debug.log(`[VideoLifecycle][session:${sessionId}] src handling finished`); } }; //#endregion //#region src/core/videoLifecycleHost.ts function createVideoLifecycleHost(handler, resolveOverlayMount) { const self = () => handler; return { get video() { return self().video; }, get site() { return self().site; }, get container() { return self().container; }, set container(value) { if (self().container === value) return; self().container = value; self().uiManager.updateMount(resolveOverlayMount(value)); }, get firstPlay() { return self().firstPlay; }, set firstPlay(value) { self().firstPlay = value; }, stopTranslation: () => handler.stopTranslation(), get uiManager() { return self().uiManager; }, getVideoData: () => handler.getVideoData(), cacheManager: { getSubtitles: (key) => self().cacheManager.getSubtitles(key) }, getSubtitlesCacheKey: (videoId, detectedLanguage, subtitleLanguage) => handler.getSubtitlesCacheKey(videoId, detectedLanguage, subtitleLanguage), getPreferredSubtitlesLanguage: (detectedLanguage, responseLanguage) => handler.getPreferredSubtitlesLanguage(detectedLanguage, responseLanguage), updateSubtitlesLangSelect: () => handler.updateSubtitlesLangSelect(), enableSubtitlesForCurrentLangPair: () => handler.enableSubtitlesForCurrentLangPair(), setSelectMenuValues: (from, to) => handler.setSelectMenuValues(from, to), get translateToLang() { return self().translateToLang; }, set translateToLang(value) { self().translateToLang = value; }, get data() { return self().data ?? {}; }, get subtitles() { return self().subtitles; }, set subtitles(value) { self().subtitles = value; }, get subtitlesCacheKey() { return self().subtitlesCacheKey; }, set subtitlesCacheKey(value) { self().subtitlesCacheKey = value; }, get videoData() { return self().videoData; }, set videoData(value) { const handler = self(); if (handler.videoData?.videoId !== value?.videoId) handler.downloadTranslation = null; handler.videoData = value; }, get actionsAbortController() { return self().actionsAbortController; }, set actionsAbortController(value) { self().actionsAbortController = value; }, resetActionsAbortController: (reason) => handler.resetActionsAbortController(reason), translationOrchestrator: handler.translationOrchestrator, resetSubtitlesWidget: () => handler.resetSubtitlesWidget(), queueOverlayAutoHide: () => handler.overlayVisibility?.queueAutoHide() }; } //#endregion //#region src/utils/text.ts var MAX_TEXT_LENGTH = 450; var REMOVABLE_TOKEN_FILTER = new RegExp([ String.raw`(?:https?:\/\/|www\.)\S+`, String.raw`#[^\s#]+`, String.raw`auto-generated\s+by\s+youtube`, String.raw`provided\s+to\s+youtube\s+by`, String.raw`released\s+on`, String.raw`\bpaypal\b`, String.raw`\b0x[a-f0-9]{40}\b`, String.raw`\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b`, String.raw`\b(?:bc1|tb1|bcrt1)[ac-hj-np-z02-9]{11,71}\b`, String.raw`\b(?:-1|0):[a-f0-9]{64}\b` ].join("|"), "giu"); var NOISE_CHARACTER_FILTER = /[\p{N}\p{P}\p{S}]+/gu; var WHITESPACE_FILTER = /\s+/g; var LETTER_FILTER = /\p{L}/u; function trimToMaxLength(text, maxLength) { if (text.length <= maxLength) return text; return text.slice(0, maxLength).trimEnd(); } function cleanText(title, description) { const raw = `${title ?? ""} ${description ?? ""}`.trim(); if (!raw) return ""; const cleaned = raw.normalize("NFKC").replace(REMOVABLE_TOKEN_FILTER, " ").replace(NOISE_CHARACTER_FILTER, " ").replace(WHITESPACE_FILTER, " ").trim(); if (!LETTER_FILTER.test(cleaned)) return ""; return trimToMaxLength(cleaned, MAX_TEXT_LENGTH); } //#endregion //#region src/utils/volume.ts var VIDEO_VOLUME_MIN_PERCENT = 0; var VIDEO_VOLUME_MAX_PERCENT = 100; var VIDEO_VOLUME_STEP_01 = .01; var EPS = 1e-6; function clampInt(value, min, max) { return Math.trunc(clampNumber(value, min, max)); } function clampPercentInt(value, min = VIDEO_VOLUME_MIN_PERCENT, max = VIDEO_VOLUME_MAX_PERCENT) { if (!Number.isFinite(value)) return min; return clampInt(Math.round(value), min, max); } function volume01ToPercent(volume01) { return clampPercentInt(clampNumber(volume01, 0, 1) * 100); } function percentToVolume01(percent) { return clampPercentInt(percent) / 100; } function quantizeToStep(value, step, direction) { if (!Number.isFinite(value)) return value; if (!Number.isFinite(step) || step <= 0) return value; const inv = 1 / step; const scaled = value * inv; switch (direction) { case "down": return Math.floor(scaled + EPS) / inv; case "up": return Math.ceil(scaled - EPS) / inv; default: return Math.round(scaled) / inv; } } function snapVolume01(volume01, direction = "nearest", step = VIDEO_VOLUME_STEP_01) { return clampNumber(quantizeToStep(clampNumber(volume01, 0, 1), step, direction), 0, 1); } function snapVolume01Towards(next, current, desired, step = VIDEO_VOLUME_STEP_01) { const cur = clampNumber(current, 0, 1); const des = clampNumber(desired, 0, 1); if (des < cur) { const q = snapVolume01(next, "down", step); return Math.max(des, q); } if (des > cur) { const q = snapVolume01(next, "up", step); return Math.min(des, q); } return snapVolume01(next, "nearest", step); } //#endregion //#region src/core/hostPolicies.ts var EXTERNAL_VOLUME_HOSTS = /* @__PURE__ */ new Set(["youtube", "googledrive"]); var YOUTUBE_LIKE_HOSTS = EXTERNAL_VOLUME_HOSTS; var MUTE_SYNC_DISABLED_HOSTS = /* @__PURE__ */ new Set(["rutube", "ok"]); var TRANSLATION_DOWNLOAD_HOSTS = /* @__PURE__ */ new Set([ "youtube", "invidious", "piped" ]); function isExternalVolumeHost(host) { return EXTERNAL_VOLUME_HOSTS.has(host); } function isYouTubeLikeHost(host) { return YOUTUBE_LIKE_HOSTS.has(host); } function isMuteSyncDisabledHost(host) { return MUTE_SYNC_DISABLED_HOSTS.has(host); } function isDesktopYouTubeLikeSite(site) { return isYouTubeLikeHost(site.host) && site.additionalData !== "mobile"; } function isTranslationDownloadHost(host) { return TRANSLATION_DOWNLOAD_HOSTS.has(host); } //#endregion //#region src/core/videoManager.ts var FORCED_DETECTED_LANGUAGE_BY_HOST = { rutube: "ru", "ok.ru": "ru", mail_ru: "ru", weverse: "ko", niconico: "ja", youku: "zh", bilibili: "zh", weibo: "zh", zdf: "de" }; var YT_VOLUME_NOW_SELECTOR = ".ytp-volume-panel [aria-valuenow]"; var YT_PLAYER_VOLUME_STORAGE_KEY = "yt-player-volume"; var MIN_DETECT_TEXT_LENGTH = 35; var MAX_SHARED_LANGUAGE_STATES = 500; var REQUEST_LANG_SET = new Set(availableLangs); var SUPPORTED_TRANSLATION_SOURCE_LANGS = /* @__PURE__ */ new Set([ "ru", "en", "zh", "ko", "fr", "it", "es", "de", "ja" ]); /** * Prevents YouTube from persisting a temporary volume level to localStorage. * * YouTube reads `yt-player-volume` on player init to restore the previous * volume. When the plugin temporarily lowers the volume (smart ducking), * YouTube would save the lowered value, and the next video would start at * that reduced level. This function saves the current localStorage value * before the action and restores it after, so YouTube's persisted volume * stays at the user's preferred level. */ function preserveYoutubeVolumeStorage(action) { let snapshot; try { snapshot = globalThis.localStorage.getItem(YT_PLAYER_VOLUME_STORAGE_KEY); } catch { snapshot = void 0; } try { return action(); } finally { if (snapshot !== void 0) try { if (snapshot === null) globalThis.localStorage.removeItem(YT_PLAYER_VOLUME_STORAGE_KEY); else globalThis.localStorage.setItem(YT_PLAYER_VOLUME_STORAGE_KEY, snapshot); } catch {} } } /** * Shared language caches across VideoManager instances within one frame. * * YouTube Shorts can transiently create multiple video handlers while the URL * (and therefore resolved `videoId`) still points to the same active short. * Per-instance caches are insufficient in that case and can trigger duplicate * language detection requests. */ var sharedLanguageStateByVideoId = /* @__PURE__ */ new Map(); function getSharedLanguageState(videoId) { const cachedState = sharedLanguageStateByVideoId.get(videoId); if (cachedState) return cachedState; const createdState = {}; sharedLanguageStateByVideoId.set(videoId, createdState); while (sharedLanguageStateByVideoId.size > MAX_SHARED_LANGUAGE_STATES) { const oldestVideoId = sharedLanguageStateByVideoId.keys().next().value; if (typeof oldestVideoId !== "string") break; sharedLanguageStateByVideoId.delete(oldestVideoId); } return createdState; } function normalizeToRequestLang(value) { if (typeof value !== "string") return void 0; const normalized = value.toLowerCase().split(/[-_]/)[0]; return REQUEST_LANG_SET.has(normalized) ? normalized : void 0; } function isResolvedLanguage(value) { return Boolean(value && value !== "auto"); } function resolveSupportedYoutubeAudioLanguage() { const response = YoutubeHelper.getPlayerResponse(); const selected = selectSmallestAudioFormat([...Array.isArray(response?.streamingData?.adaptiveFormats) ? response.streamingData.adaptiveFormats : [], ...Array.isArray(response?.streamingData?.formats) ? response.streamingData.formats : []].filter((format) => { const mimeType = String(format?.mimeType ?? ""); return mimeType.includes("audio/") && !mimeType.includes("video/"); }).map((format) => { return { language: normalizeToRequestLang(getYoutubeAudioFormatLanguage(format, (value) => Boolean(normalizeToRequestLang(value)))), contentLength: format?.contentLength, averageBitrate: format?.averageBitrate }; }).filter((c) => c.language && SUPPORTED_TRANSLATION_SOURCE_LANGS.has(c.language))); return selected?.language && isResolvedLanguage(selected.language) ? selected.language : void 0; } function buildDetectText(title, description) { return cleanText(typeof title === "string" ? title : "", typeof description === "string" ? description : void 0); } function resolveHostDetectedLanguage(host) { const forcedDetectedLanguage = FORCED_DETECTED_LANGUAGE_BY_HOST[host]; if (forcedDetectedLanguage) return forcedDetectedLanguage; if (host === "vk") { const trackLang = document.getElementsByTagName("track")?.[0]?.srclang; return normalizeToRequestLang(trackLang); } } function resolveYoutubeDetectedLanguageFromSubtitles(subtitles) { if (!Array.isArray(subtitles)) return; const candidates = subtitles.filter((subtitle) => Boolean(subtitle) && typeof subtitle === "object" && subtitle.source === "youtube" && typeof subtitle.translatedFromLanguage !== "string"); const pickLanguage = (predicate) => candidates.filter(predicate).map((candidate) => normalizeToRequestLang(candidate.language)).find(isResolvedLanguage); return pickLanguage((candidate) => candidate.isAutoGenerated !== true) ?? pickLanguage(() => true); } async function resolveDetectedLanguageForVideo(options) { if (options.isStream) return { detectedLanguage: "auto" }; if (options.userOverrideLanguage) return { detectedLanguage: options.userOverrideLanguage }; const hostDetectedLanguage = resolveHostDetectedLanguage(options.host); if (isResolvedLanguage(hostDetectedLanguage)) return { detectedLanguage: hostDetectedLanguage, cacheLanguage: hostDetectedLanguage }; const normalizedPossibleLanguage = normalizeToRequestLang(options.possibleLanguage); if (isResolvedLanguage(normalizedPossibleLanguage)) return { detectedLanguage: normalizedPossibleLanguage, cacheLanguage: normalizedPossibleLanguage }; const youtubeSubtitleDetectedLanguage = options.host === "youtube" ? resolveYoutubeDetectedLanguageFromSubtitles(options.subtitles) : void 0; if (isResolvedLanguage(youtubeSubtitleDetectedLanguage)) return { detectedLanguage: youtubeSubtitleDetectedLanguage, cacheLanguage: youtubeSubtitleDetectedLanguage }; if (options.cachedDetectedLanguage) return { detectedLanguage: options.cachedDetectedLanguage }; if (!options.allowTextLanguageDetection) return { detectedLanguage: "auto" }; const text = buildDetectText(options.title, options.description); if (!text || text.length < MIN_DETECT_TEXT_LENGTH) return { detectedLanguage: "auto" }; const detectedLanguage = await options.detectLanguage(text); if (!detectedLanguage) return { detectedLanguage: "auto" }; return { detectedLanguage, cacheLanguage: detectedLanguage }; } function getAriaValueNowPercent(selector) { const el = document.querySelector(selector); const rawNow = el?.getAttribute("aria-valuenow"); const rawMax = el?.getAttribute("aria-valuemax"); const now = rawNow == null ? NaN : Number.parseFloat(rawNow); const max = rawMax == null ? NaN : Number.parseFloat(rawMax); if (!Number.isFinite(now)) return null; if (Number.isFinite(max) && max > 0) return clampPercentInt(now / max * 100); return clampPercentInt(now); } var VOTVideoManager = class { videoHandler; constructor(videoHandler) { this.videoHandler = videoHandler; } setDetectedLanguageCache(videoId, language) { getSharedLanguageState(videoId).detectedLanguage = language; } rememberUserLanguageSelection(videoId, language) { const normalizedLanguage = normalizeToRequestLang(language); if (!isResolvedLanguage(normalizedLanguage)) { const sharedLanguageState = sharedLanguageStateByVideoId.get(videoId); if (sharedLanguageState) delete sharedLanguageState.userLanguageOverride; return; } const sharedLanguageState = getSharedLanguageState(videoId); sharedLanguageState.userLanguageOverride = normalizedLanguage; sharedLanguageState.detectedLanguage = normalizedLanguage; } rememberDetectedLanguage(videoId, language) { const normalizedLanguage = normalizeToRequestLang(language); if (!isResolvedLanguage(normalizedLanguage)) return; this.setDetectedLanguageCache(videoId, normalizedLanguage); if (this.videoHandler.videoData?.videoId === videoId) this.videoHandler.videoData.detectedLanguage = normalizedLanguage; } async detectLanguageSingleFlight(videoId, text) { const sharedLanguageState = getSharedLanguageState(videoId); const inFlightDetect = sharedLanguageState.detectInFlight; if (inFlightDetect !== void 0) return inFlightDetect; const task = (async () => { debug.log(`Detecting language text: ${text}`); const language = normalizeToRequestLang(await detect(text)); return isResolvedLanguage(language) ? language : void 0; })(); sharedLanguageState.detectInFlight = task; try { return await task; } finally { if (sharedLanguageState.detectInFlight === task) delete sharedLanguageState.detectInFlight; } } async resolveVideoLanguage({ videoId, isStream, possibleLanguage, subtitles, title, description, allowTextLanguageDetection }) { const sharedLanguageState = getSharedLanguageState(videoId); const result = await resolveDetectedLanguageForVideo({ isStream, host: this.videoHandler.site.host, possibleLanguage, subtitles, userOverrideLanguage: sharedLanguageState.userLanguageOverride, cachedDetectedLanguage: sharedLanguageState.detectedLanguage, title, description, allowTextLanguageDetection, detectLanguage: async (text) => await this.detectLanguageSingleFlight(videoId, text) }); if (result.cacheLanguage) this.setDetectedLanguageCache(videoId, result.cacheLanguage); return { ...result, sharedLanguageState }; } async ensureDetectedLanguageForTranslation(videoData) { if (!videoData?.videoId) return; if (videoData.detectedLanguage === "auto") { const { detectedLanguage } = await this.resolveVideoLanguage({ videoId: videoData.videoId, isStream: videoData.isStream, possibleLanguage: videoData.detectedLanguage, subtitles: videoData.subtitles, title: videoData.title, description: videoData.description, allowTextLanguageDetection: true }); if (detectedLanguage && detectedLanguage !== "auto") videoData.detectedLanguage = detectedLanguage; } const detected = normalizeToRequestLang(videoData.detectedLanguage); if (detected && detected !== "auto" && SUPPORTED_TRANSLATION_SOURCE_LANGS.has(detected)) return; if (this.videoHandler.site.host !== "youtube") return; const supportedVideoLanguage = resolveSupportedYoutubeAudioLanguage(); if (!supportedVideoLanguage) { debug.log("[language] no supported YouTube audio track found", { videoId: videoData.videoId, detectedLanguage: videoData.detectedLanguage }); return; } const previousLanguage = videoData.detectedLanguage; videoData.detectedLanguage = supportedVideoLanguage; this.setDetectedLanguageCache(videoData.videoId, supportedVideoLanguage); if (this.videoHandler.translateFromLang === "auto") { this.videoHandler.translateFromLang = supportedVideoLanguage; this.videoHandler.autoSourceLanguageOverrideVideoId = videoData.videoId; this.videoHandler.setSelectMenuValues(supportedVideoLanguage, videoData.responseLanguage); } debug.log("[language] unsupported language switched immediately", { videoId: videoData.videoId, previousLanguage, supportedVideoLanguage, translateFromLang: this.videoHandler.translateFromLang }); } async getVideoData() { const { duration, url, videoId, host, title, translationHelp = null, localizedTitle, description, detectedLanguage: possibleLanguage, subtitles, isStream = false } = await getVideoData(this.videoHandler.site, { fetchFn: GM_fetch, video: this.videoHandler.video, language: localizationProvider.lang }); const { detectedLanguage, sharedLanguageState } = await this.resolveVideoLanguage({ videoId, isStream, possibleLanguage, subtitles, title, description, allowTextLanguageDetection: false }); const videoData = { translationHelp, isStream, duration: duration || this.videoHandler.video?.duration || config_default$1.defaultDuration, videoId, url, host, detectedLanguage, responseLanguage: this.videoHandler.translateToLang, subtitles, title, localizedTitle, description, downloadTitle: localizedTitle ?? title ?? document.title ?? videoId }; if (sharedLanguageState.lastLoggedDetectedLanguage !== detectedLanguage) { debug.log("[VOT] Detected language:", detectedLanguage); sharedLanguageState.lastLoggedDetectedLanguage = detectedLanguage; } return videoData; } async videoValidator() { const videoData = this.videoHandler.videoData; const data = this.videoHandler.data; if (!videoData || !data) throw new VOTLocalizedError("VOTNoVideoIDFound"); debug.log("VideoValidator videoData: ", this.videoHandler.videoData); if (this.videoHandler.data.enabledDontTranslateLanguages && this.videoHandler.data.dontTranslateLanguages?.includes(this.videoHandler.videoData.detectedLanguage)) throw new VOTLocalizedError("VOTDisableFromYourLang"); if (this.videoHandler.videoData.isStream) throw new VOTLocalizedError("VOTStreamNotAvailable"); if (this.videoHandler.videoData.duration > 14400) throw new VOTLocalizedError("VOTVideoIsTooLong"); return true; } /** * Gets current video volume (0.0 - 1.0) */ getVideoVolume() { const video = this.videoHandler.video; if (!video) return void 0; if (isExternalVolumeHost(this.videoHandler.site.host)) { const ariaPercent = getAriaValueNowPercent(YT_VOLUME_NOW_SELECTOR); if (ariaPercent != null) return percentToVolume01(ariaPercent); const extVolume = YoutubeHelper.getVolume(); if (typeof extVolume === "number" && Number.isFinite(extVolume)) return snapVolume01(extVolume); } return snapVolume01(video.volume); } /** * Sets the video volume */ setVideoVolume(volume, options = {}) { const snapped = snapVolume01(volume); if (!isExternalVolumeHost(this.videoHandler.site.host)) { this.videoHandler.video.volume = snapped; return this; } try { const setExternalVolume = () => YoutubeHelper.setVolume(snapped); const result = options.preserveYoutubeVolumeStorage ? preserveYoutubeVolumeStorage(setExternalVolume) : setExternalVolume(); if (typeof result === "boolean" && result || typeof result === "number" && Number.isFinite(result)) return this; } catch {} this.videoHandler.video.volume = snapped; return this; } setVideoMuted(muted) { if (this.videoHandler.video) this.videoHandler.video.muted = muted; return this; } /** * Checks if the video is muted */ isMuted() { if (!isExternalVolumeHost(this.videoHandler.site.host)) return this.videoHandler.video?.muted; return YoutubeHelper.isMuted() || Boolean(this.videoHandler.video?.muted); } /** * Syncs the video volume slider with the actual video volume. */ syncVideoVolumeSlider() { const overlayView = this.videoHandler.uiManager.votOverlayView; if (!overlayView?.isInitialized()) return this; const ariaPercent = isExternalVolumeHost(this.videoHandler.site.host) ? getAriaValueNowPercent(YT_VOLUME_NOW_SELECTOR) : null; const volumePercent = this.isMuted() ? 0 : ariaPercent ?? volume01ToPercent(this.getVideoVolume() ?? 0); overlayView.videoVolumeSlider.value = volumePercent; this.videoHandler.onVideoVolumeSliderSynced?.(volumePercent); return this; } setSelectMenuValues(from, to) { const videoData = this.videoHandler.videoData; if (!videoData) return this; const normalizedFrom = normalizeToRequestLang(from) ?? "auto"; const langPairLogKey = `${normalizedFrom}->${to}`; const sharedLanguageState = getSharedLanguageState(videoData.videoId); if (sharedLanguageState.lastLoggedLangPair !== langPairLogKey) { debug.log(`[VOT] Set translation from ${normalizedFrom} to ${to}`); sharedLanguageState.lastLoggedLangPair = langPairLogKey; } videoData.detectedLanguage = normalizedFrom; videoData.responseLanguage = to; this.videoHandler.translateFromLang = normalizedFrom; this.videoHandler.translateToLang = to; const overlayView = this.videoHandler.uiManager.votOverlayView; if (!overlayView?.isInitialized()) return this; overlayView.languagePairSelect.fromSelect.selectTitle = localizationProvider.getLangLabel(normalizedFrom); overlayView.languagePairSelect.toSelect.selectTitle = localizationProvider.getLangLabel(to); overlayView.languagePairSelect.fromSelect.setSelectedValue(normalizedFrom); overlayView.languagePairSelect.toSelect.setSelectedValue(to); return this; } }; //#endregion //#region src/notify.ts var now = () => Date.now(); function getScriptTitle() { return GM_info?.script?.name || "VOT"; } function safeL10n(key, fallback) { try { return localizationProvider?.get?.(key) || fallback; } catch { return fallback; } } function canSend(lastSentAt, key, cooldownMs) { if (!cooldownMs) return true; const prev = lastSentAt.get(key) ?? 0; return now() - prev >= cooldownMs; } function markSent(lastSentAt, key) { lastSentAt.set(key, now()); } function localizePhraseText(message) { const key = message.trim(); if (!key) return null; try { const localized = localizationProvider.get(key); const defaultText = localizationProvider.getDefault(key); return localized !== key || defaultText !== key ? localized || defaultText || key : null; } catch { return null; } } function resolveLocalizedErrorFromObject(message) { if (!message || typeof message !== "object") return null; const localizedError = message; if (localizedError.name !== "VOTLocalizedError") return null; if (typeof localizedError.localizedMessage === "string" && localizedError.localizedMessage.trim()) return localizedError.localizedMessage; if (typeof localizedError.unlocalizedMessage === "string") return localizePhraseText(localizedError.unlocalizedMessage); return null; } function localizeExtractedErrorMessage(message) { const extracted = getErrorMessage(message); if (!extracted) return null; return localizePhraseText(extracted) || extracted; } function resolveLocalizedErrorMessage(message) { const localizedObjectMessage = resolveLocalizedErrorFromObject(message); if (localizedObjectMessage) return localizedObjectMessage; if (typeof message === "string") { const byPhraseKey = localizePhraseText(message); if (byPhraseKey) return byPhraseKey; } const extractedMessage = localizeExtractedErrorMessage(message); if (extractedMessage) return extractedMessage; return safeL10n("requestTranslationFailed", "Translation failed"); } function trySendViaUserscriptApi(details) { try { if (typeof GM_notification === "function") { GM_notification(details); return true; } const gmApi = globalThis.GM; if (gmApi !== void 0 && typeof gmApi.notification === "function") { const gmDetails = { text: details.text, title: details.title, image: details.image, onclick: details.onclick, ondone: details.ondone }; gmApi.notification(gmDetails); return true; } } catch (err) { debug.log("[notify] userscript api error", err); } return false; } /** * Notification helper with dedupe/rate-limit and safe fallbacks. */ var Notifier = class { lastSentAt = /* @__PURE__ */ new Map(); send(details, opts = {}) { try { const key = opts.key || details.tag || `${details.title ?? ""}|${details.text ?? ""}`; const cooldownMs = opts.cooldownMs ?? 0; if (!canSend(this.lastSentAt, key, cooldownMs)) return; const normalized = { ...details, title: details.title ?? getScriptTitle() }; if (trySendViaUserscriptApi(normalized)) markSent(this.lastSentAt, key); else debug.log("[notify] unavailable", normalized); } catch (err) { debug.log("[notify] send error", err); } } translationCompleted(host) { const text = safeL10n("VOTTranslationCompletedNotify", "The translation on the {0} has been completed!").replace("{0}", host); this.send({ text, title: getScriptTitle(), timeout: 5e3, silent: true, tag: "VOTTranslationCompleted", onclick: () => { try { globalThis.focus(); } catch {} } }, { key: `translation_completed_${host}`, cooldownMs: 1e4 }); } translationFailed(params) { const { videoId, message } = params; if (isAbortError(message)) return; const msg = resolveLocalizedErrorMessage(message); const title = getScriptTitle(); this.send({ text: msg, title, timeout: 8e3, silent: true, tag: `VOTtranslationFailed_${videoId || "unknown"}`, onclick: () => { try { globalThis.focus(); } catch {} } }, { key: `translation_failed_${videoId || "unknown"}`, cooldownMs: 3e4 }); } }; //#endregion //#region node_modules/lit-html/lit-html.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t = globalThis; var i = (t) => t; var s = t.trustedTypes; var e = s ? s.createPolicy("lit-html", { createHTML: (t) => t }) : void 0; var h = "$lit$"; var o = `lit$${Math.random().toFixed(9).slice(2)}$`; var n = "?" + o; var r = `<${n}>`; var l = document; var c = () => l.createComment(""); var a = (t) => null === t || "object" != typeof t && "function" != typeof t; var u = Array.isArray; var d = (t) => u(t) || "function" == typeof t?.[Symbol.iterator]; var f = "[ \n\f\r]"; var v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; var _ = /-->/g; var m = />/g; var p = RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"); var g = /'/g; var $ = /"/g; var y = /^(?:script|style|textarea|title)$/i; var x = (t) => (i, ...s) => ({ _$litType$: t, strings: i, values: s }); var b = x(1); var w = x(2); var E = Symbol.for("lit-noChange"); var A = Symbol.for("lit-nothing"); var C = /* @__PURE__ */ new WeakMap(); var P = l.createTreeWalker(l, 129); function V(t, i) { if (!u(t) || !t.hasOwnProperty("raw")) throw Error("invalid template strings array"); return void 0 !== e ? e.createHTML(i) : i; } var N = (t, i) => { const s = t.length - 1, e = []; let n, l = 2 === i ? "" : 3 === i ? "" : "", c = v; for (let i = 0; i < s; i++) { const s = t[i]; let a, u, d = -1, f = 0; for (; f < s.length && (c.lastIndex = f, u = c.exec(s), null !== u);) f = c.lastIndex, c === v ? "!--" === u[1] ? c = _ : void 0 !== u[1] ? c = m : void 0 !== u[2] ? (y.test(u[2]) && (n = RegExp("" === u[0] ? (c = n ?? v, d = -1) : void 0 === u[1] ? d = -2 : (d = c.lastIndex - u[2].length, a = u[1], c = void 0 === u[3] ? p : "\"" === u[3] ? $ : g) : c === $ || c === g ? c = p : c === _ || c === m ? c = v : (c = p, n = void 0); const x = c === p && t[i + 1].startsWith("/>") ? " " : ""; l += c === v ? s + r : d >= 0 ? (e.push(a), s.slice(0, d) + h + s.slice(d) + o + x) : s + o + (-2 === d ? i : x); } return [V(t, l + (t[s] || "") + (2 === i ? "" : 3 === i ? "" : "")), e]; }; var S = class S { constructor({ strings: t, _$litType$: i }, e) { let r; this.parts = []; let l = 0, a = 0; const u = t.length - 1, d = this.parts, [f, v] = N(t, i); if (this.el = S.createElement(f, e), P.currentNode = this.el.content, 2 === i || 3 === i) { const t = this.el.content.firstChild; t.replaceWith(...t.childNodes); } for (; null !== (r = P.nextNode()) && d.length < u;) { if (1 === r.nodeType) { if (r.hasAttributes()) for (const t of r.getAttributeNames()) if (t.endsWith(h)) { const i = v[a++], s = r.getAttribute(t).split(o), e = /([.?@])?(.*)/.exec(i); d.push({ type: 1, index: l, name: e[2], strings: s, ctor: "." === e[1] ? I : "?" === e[1] ? L : "@" === e[1] ? z : H }), r.removeAttribute(t); } else t.startsWith(o) && (d.push({ type: 6, index: l }), r.removeAttribute(t)); if (y.test(r.tagName)) { const t = r.textContent.split(o), i = t.length - 1; if (i > 0) { r.textContent = s ? s.emptyScript : ""; for (let s = 0; s < i; s++) r.append(t[s], c()), P.nextNode(), d.push({ type: 2, index: ++l }); r.append(t[i], c()); } } } else if (8 === r.nodeType) if (r.data === n) d.push({ type: 2, index: l }); else { let t = -1; for (; -1 !== (t = r.data.indexOf(o, t + 1));) d.push({ type: 7, index: l }), t += o.length - 1; } l++; } } static createElement(t, i) { const s = l.createElement("template"); return s.innerHTML = t, s; } }; function M(t, i, s = t, e) { if (i === E) return i; let h = void 0 !== e ? s._$Co?.[e] : s._$Cl; const o = a(i) ? void 0 : i._$litDirective$; return h?.constructor !== o && (h?._$AO?.(!1), void 0 === o ? h = void 0 : (h = new o(t), h._$AT(t, s, e)), void 0 !== e ? (s._$Co ??= [])[e] = h : s._$Cl = h), void 0 !== h && (i = M(t, h._$AS(t, i.values), h, e)), i; } var R = class { constructor(t, i) { this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = i; } get parentNode() { return this._$AM.parentNode; } get _$AU() { return this._$AM._$AU; } u(t) { const { el: { content: i }, parts: s } = this._$AD, e = (t?.creationScope ?? l).importNode(i, !0); P.currentNode = e; let h = P.nextNode(), o = 0, n = 0, r = s[0]; for (; void 0 !== r;) { if (o === r.index) { let i; 2 === r.type ? i = new k(h, h.nextSibling, this, t) : 1 === r.type ? i = new r.ctor(h, r.name, r.strings, this, t) : 6 === r.type && (i = new Z(h, this, t)), this._$AV.push(i), r = s[++n]; } o !== r?.index && (h = P.nextNode(), o++); } return P.currentNode = l, e; } p(t) { let i = 0; for (const s of this._$AV) void 0 !== s && (void 0 !== s.strings ? (s._$AI(t, s, i), i += s.strings.length - 2) : s._$AI(t[i])), i++; } }; var k = class k { get _$AU() { return this._$AM?._$AU ?? this._$Cv; } constructor(t, i, s, e) { this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t, this._$AB = i, this._$AM = s, this.options = e, this._$Cv = e?.isConnected ?? !0; } get parentNode() { let t = this._$AA.parentNode; const i = this._$AM; return void 0 !== i && 11 === t?.nodeType && (t = i.parentNode), t; } get startNode() { return this._$AA; } get endNode() { return this._$AB; } _$AI(t, i = this) { t = M(this, t, i), a(t) ? t === A || null == t || "" === t ? (this._$AH !== A && this._$AR(), this._$AH = A) : t !== this._$AH && t !== E && this._(t) : void 0 !== t._$litType$ ? this.$(t) : void 0 !== t.nodeType ? this.T(t) : d(t) ? this.k(t) : this._(t); } O(t) { return this._$AA.parentNode.insertBefore(t, this._$AB); } T(t) { this._$AH !== t && (this._$AR(), this._$AH = this.O(t)); } _(t) { this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t : this.T(l.createTextNode(t)), this._$AH = t; } $(t) { const { values: i, _$litType$: s } = t, e = "number" == typeof s ? this._$AC(t) : (void 0 === s.el && (s.el = S.createElement(V(s.h, s.h[0]), this.options)), s); if (this._$AH?._$AD === e) this._$AH.p(i); else { const t = new R(e, this), s = t.u(this.options); t.p(i), this.T(s), this._$AH = t; } } _$AC(t) { let i = C.get(t.strings); return void 0 === i && C.set(t.strings, i = new S(t)), i; } k(t) { u(this._$AH) || (this._$AH = [], this._$AR()); const i = this._$AH; let s, e = 0; for (const h of t) e === i.length ? i.push(s = new k(this.O(c()), this.O(c()), this, this.options)) : s = i[e], s._$AI(h), e++; e < i.length && (this._$AR(s && s._$AB.nextSibling, e), i.length = e); } _$AR(t = this._$AA.nextSibling, s) { for (this._$AP?.(!1, !0, s); t !== this._$AB;) { const s = i(t).nextSibling; i(t).remove(), t = s; } } setConnected(t) { void 0 === this._$AM && (this._$Cv = t, this._$AP?.(t)); } }; var H = class { get tagName() { return this.element.tagName; } get _$AU() { return this._$AM._$AU; } constructor(t, i, s, e, h) { this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t, this.name = i, this._$AM = e, this.options = h, s.length > 2 || "" !== s[0] || "" !== s[1] ? (this._$AH = Array(s.length - 1).fill(/* @__PURE__ */ new String()), this.strings = s) : this._$AH = A; } _$AI(t, i = this, s, e) { const h = this.strings; let o = !1; if (void 0 === h) t = M(this, t, i, 0), o = !a(t) || t !== this._$AH && t !== E, o && (this._$AH = t); else { const e = t; let n, r; for (t = h[0], n = 0; n < h.length - 1; n++) r = M(this, e[s + n], i, n), r === E && (r = this._$AH[n]), o ||= !a(r) || r !== this._$AH[n], r === A ? t = A : t !== A && (t += (r ?? "") + h[n + 1]), this._$AH[n] = r; } o && !e && this.j(t); } j(t) { t === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); } }; var I = class extends H { constructor() { super(...arguments), this.type = 3; } j(t) { this.element[this.name] = t === A ? void 0 : t; } }; var L = class extends H { constructor() { super(...arguments), this.type = 4; } j(t) { this.element.toggleAttribute(this.name, !!t && t !== A); } }; var z = class extends H { constructor(t, i, s, e, h) { super(t, i, s, e, h), this.type = 5; } _$AI(t, i = this) { if ((t = M(this, t, i, 0) ?? A) === E) return; const s = this._$AH, e = t === A && s !== A || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, h = t !== A && (s === A || e); e && this.element.removeEventListener(this.name, this, s), h && this.element.addEventListener(this.name, this, t), this._$AH = t; } handleEvent(t) { "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t) : this._$AH.handleEvent(t); } }; var Z = class { constructor(t, i, s) { this.element = t, this.type = 6, this._$AN = void 0, this._$AM = i, this.options = s; } get _$AU() { return this._$AM._$AU; } _$AI(t) { M(this, t); } }; var B = t.litHtmlPolyfillSupport; B?.(S, k), (t.litHtmlVersions ??= []).push("3.3.3"); var D = (t, i, s) => { const e = s?.renderBefore ?? i; let h = e._$litPart$; if (void 0 === h) { const t = s?.renderBefore ?? null; e._$litPart$ = h = new k(i.insertBefore(c(), t), t, void 0, s ?? {}); } return h._$AI(t), h; }; //#endregion //#region src/ui/components/componentShared.ts function setInteractiveHiddenState(element, isHidden) { element.hidden = isHidden; element.setAttribute("aria-hidden", isHidden ? "true" : "false"); element.toggleAttribute("inert", isHidden); } function createDomId(prefix) { return `${prefix}-${typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2)}`; } function isEventInside(event, element) { const target = event.target; if (target instanceof Node && element.contains(target)) return true; return typeof event.composedPath === "function" && event.composedPath().includes(element); } function isPrimaryPointerAction(event) { return event.isPrimary && event.button === 0; } function isKeyboardActivation(event) { return event.key === "Enter" || event.key === " "; } function addKeyboardActivationListener(element, handler, options) { element.addEventListener("keydown", (event) => { if (!isKeyboardActivation(event)) return; event.preventDefault(); handler(); }, options); } var UIComponent = class { container; set hidden(isHidden) { this.container.hidden = isHidden; } get hidden() { return this.container.hidden === true; } }; var UIComponentWithEvents = class extends UIComponent { events; constructor(types) { super(); this.events = Object.fromEntries(types.map((typeItem) => [typeItem, new EventImpl()])); } addEventListener(type, listener) { this.events[type].addListener(listener); return this; } removeEventListener(type, listener) { this.events[type].removeListener(listener); return this; } dispatch(type, ...args) { this.events[type].dispatch(...args); return this; } clearEventListeners() { for (const event of Object.values(this.events)) event.clear(); } }; //#endregion //#region src/ui.ts function initKeyboardNavigationMode() { if (globalThis.__votKeyboardNavInitialized) return; globalThis.__votKeyboardNavInitialized = true; const root = document.documentElement; const CLASS = "vot-keyboard-nav"; const enable = () => root.classList.add(CLASS); const disable = () => root.classList.remove(CLASS); globalThis.addEventListener("keydown", (e) => { if (e.key === "Tab") enable(); }, true); for (const evt of ["pointerdown", "touchstart"]) globalThis.addEventListener(evt, disable, { capture: true, passive: true }); } initKeyboardNavigationMode(); var UI = { /** * Makes a non-native element behave like a button (keyboard + ARIA). * * We use custom tags (`vot-block`) for isolation, so we must re-add * basic semantics for accessibility. */ makeButtonLike(el, { ariaLabel } = {}) { el.setAttribute("role", "button"); if (!el.hasAttribute("tabindex")) el.tabIndex = 0; const enabledTabIndex = el.tabIndex; const syncDisabledState = () => { if (el.getAttribute("disabled") === "true") { el.setAttribute("aria-disabled", "true"); el.tabIndex = -1; } else { el.removeAttribute("aria-disabled"); el.tabIndex = enabledTabIndex; } }; syncDisabledState(); new MutationObserver(() => syncDisabledState()).observe(el, { attributes: true, attributeFilter: ["disabled"] }); if (ariaLabel) el.setAttribute("aria-label", ariaLabel); addKeyboardActivationListener(el, () => { if (el.getAttribute("disabled") === "true" || el.getAttribute("aria-disabled") === "true") return; el.click(); }); return el; }, /** * Auxiliary method for creating HTML elements */ createEl(tag, classes = [], content = null) { const el = document.createElement(tag); if (classes.length) el.classList.add(...classes); if (content !== null) el.append(content); return el; }, /** * Create header element */ createHeader(html, level = 4) { return UI.createEl("vot-block", ["vot-header", `vot-header-level-${level}`], html); }, /** * Create information element */ createInformation(labelHtml, valueHtml) { const container = UI.createEl("vot-block", ["vot-info"]); const header = UI.createEl("vot-block"); D(labelHtml, header); const value = UI.createEl("vot-block"); D(valueHtml, value); container.append(header, value); return { container, header, value }; }, /** * Create button */ createButton(html) { const el = UI.createEl("vot-block", ["vot-button"], html); return UI.makeButtonLike(el); }, /** * Create text button */ createTextButton(html) { const el = UI.createEl("vot-block", ["vot-text-button"], html); return UI.makeButtonLike(el); }, /** * Create outlined button */ createOutlinedButton(html) { const el = UI.createEl("vot-block", ["vot-outlined-button"], html); return UI.makeButtonLike(el); }, /** * Create icon button */ createIconButton(templateHtml, options = {}) { const button = UI.createEl("vot-block", ["vot-icon-button"]); D(templateHtml, button); return UI.makeButtonLike(button, options); }, createInlineLoader() { return UI.createEl("vot-block", ["vot-inline-loader"]); }, createPortal(local = false) { return UI.createEl("vot-block", [`vot-portal${local ? "-local" : ""}`]); }, createSubtitleInfo(word, desc, translationService) { const container = UI.createEl("vot-block", ["vot-subtitles-info"]); container.id = "vot-subtitles-info"; const translatedWith = UI.createEl("vot-block", ["vot-subtitles-info-service"], localizationProvider.get("VOTTranslatedBy").replace("{0}", translationService)); translatedWith.hidden = true; const title = UI.createEl("vot-block", ["vot-subtitles-info-title"]); const source = UI.createEl("span", ["vot-subtitles-info-source"], word); const divider = UI.createEl("span", ["vot-subtitles-info-divider"], "—"); const header = UI.createEl("span", ["vot-subtitles-info-header"], word); title.append(source, divider, header); const context = UI.createEl("vot-block", ["vot-subtitles-info-context"], desc); container.append(title, context); return { container, translatedWith, header, context }; } }; //#endregion //#region src/types/components/tooltip.ts var positions$1 = [ "left", "top", "right", "bottom" ]; var triggers = ["hover", "click"]; var tooltipModes = ["default", "follow"]; //#endregion //#region src/ui/components/tooltip.ts var DEFAULT_TOOLTIP_POS = "top"; var DEFAULT_TOOLTIP_TRIGGER = "hover"; var DEFAULT_TOOLTIP_MODE = "default"; var Tooltip = class Tooltip { showed = false; target; anchor; edgeAnchor; content; position; preferredPosition; trigger; offsetX; offsetY; _hidden; autoLayout; maxWidth; mode; backgroundColor; borderRadius; _bordered; portal; container; resizeObserver; intersectionObserver; scrollListening = false; positionRafId = null; destroyFallbackTimerId; static DESTROY_FALLBACK_MS = 700; tooltipId = createDomId("vot-tooltip"); prevAriaDescribedBy = null; constructor(opts) { const target = opts.target; if (!(target instanceof HTMLElement)) throw new TypeError("target must be a valid HTMLElement"); this.target = target; this.anchor = opts.anchor instanceof HTMLElement ? opts.anchor : target; this.edgeAnchor = opts.edgeAnchor instanceof HTMLElement ? opts.edgeAnchor : this.anchor; this.content = opts.content ?? ""; const offset = opts.offset ?? 4; if (typeof offset === "number") this.offsetY = this.offsetX = offset; else { this.offsetX = offset.x; this.offsetY = offset.y; } this._hidden = opts.hidden ?? false; this.autoLayout = opts.autoLayout ?? true; this.trigger = Tooltip.normalizeTrigger(opts.trigger); this.position = Tooltip.normalizePos(opts.position); this.preferredPosition = this.position; this.portal = opts.parentElement ?? document.body; this.borderRadius = opts.borderRadius; this._bordered = opts.bordered ?? true; this.maxWidth = opts.maxWidth; this.mode = Tooltip.normalizeMode(opts.mode); this.backgroundColor = opts.backgroundColor; this.init(); } static normalizePos(position) { return positions$1.includes(position) ? position : DEFAULT_TOOLTIP_POS; } static normalizeTrigger(trigger) { return triggers.includes(trigger) ? trigger : DEFAULT_TOOLTIP_TRIGGER; } static normalizeMode(mode) { return tooltipModes.includes(mode) ? mode : DEFAULT_TOOLTIP_MODE; } setPosition(position) { this.position = this.preferredPosition = Tooltip.normalizePos(position); this.schedulePositionUpdate(); return this; } syncContentClass() { if (!this.container) return; const isSubtitlesInfo = this.content instanceof HTMLElement && this.content.classList.contains("vot-subtitles-info"); this.container.classList.toggle("vot-tooltip--subtitles-info", isSubtitlesInfo); } setContent(content) { this.content = content; if (!this.container) return this; this.container.replaceChildren(); if (typeof content === "string") this.container.textContent = content; else this.container.append(content); this.syncContentClass(); this.schedulePositionUpdate(); return this; } /** Remove tooltip DOM immediately (no fade). Use when switching UI modes. */ dismissImmediate() { return this.destroy(true); } /** * After enabling the tooltip while the pointer never left the target, * `pointerenter` does not fire again — call this to show immediately. */ revealIfHovered() { if (this._hidden || this.trigger !== "hover") return this; try { if (!this.target.matches(":hover")) return this; } catch { return this; } this.create(); return this; } /** * Update tooltip mount dependencies. * If the tooltip is currently rendered, it will be moved to the new parent. */ updateMount({ parentElement }) { if (!(parentElement && this.portal !== parentElement)) return this; this.portal = parentElement; if (this.container?.isConnected) { parentElement.appendChild(this.container); this.schedulePositionUpdate(); } return this; } onResize = () => { this.schedulePositionUpdate(); }; onScroll = () => { this.schedulePositionUpdate(); }; onClick = () => { this.showed ? this.destroy() : this.create(); }; onDocumentPointerDown = (event) => { if (!this.showed) return; if (isEventInside(event, this.target) || this.container && isEventInside(event, this.container) || this.mode === "follow" && isEventInside(event, this.anchor)) return; this.destroy(); }; onTargetKeyDown = (event) => { if (event.key === "Escape" && this.showed) this.destroy(); }; onPointerEnter = (_e) => { this.create(); }; onPointerLeave = (e) => { if (!this.isInTooltipContext(e.relatedTarget)) this.destroy(); }; onTooltipPointerLeave = (e) => { if (!this.isInTooltipContext(e.relatedTarget)) this.destroy(); }; onTouchPointerDown = (e) => { if (e.pointerType === "touch") this.create(); }; onTouchPointerUp = (e) => { if (e.pointerType === "touch") this.destroy(); }; isInTooltipContext(nextTarget) { if (!(nextTarget instanceof Node)) return false; return this.target.contains(nextTarget) || this.container?.contains(nextTarget); } init() { this.resizeObserver = new ResizeObserver(this.onResize); this.intersectionObserver = new IntersectionObserver(this.onIntersect.bind(this)); this.target.addEventListener("keydown", this.onTargetKeyDown); if (this.trigger === "click") { this.target.addEventListener("pointerdown", this.onClick); return; } this.target.addEventListener("pointerenter", this.onPointerEnter); this.target.addEventListener("pointerleave", this.onPointerLeave); this.target.addEventListener("pointerdown", this.onTouchPointerDown); this.target.addEventListener("pointerup", this.onTouchPointerUp); } onIntersect(entries) { if (!entries[0]?.isIntersecting) this.destroy(true); } release() { this.destroy(true); this.detachScrollListener(); this.target.removeEventListener("keydown", this.onTargetKeyDown); if (this.trigger === "click") { this.target.removeEventListener("pointerdown", this.onClick); return this; } this.target.removeEventListener("pointerenter", this.onPointerEnter); this.target.removeEventListener("pointerleave", this.onPointerLeave); this.target.removeEventListener("pointerdown", this.onTouchPointerDown); this.target.removeEventListener("pointerup", this.onTouchPointerUp); return this; } schedulePositionUpdate() { if (!this.container || this.positionRafId !== null) return; this.positionRafId = requestAnimationFrame(() => { this.positionRafId = null; this.updatePos(); }); } cancelPositionUpdate() { if (this.positionRafId !== null) { cancelAnimationFrame(this.positionRafId); this.positionRafId = null; } } clearDestroyFallbackTimer() { if (this.destroyFallbackTimerId !== void 0) { globalThis.clearTimeout(this.destroyFallbackTimerId); this.destroyFallbackTimerId = void 0; } } create() { this.destroy(true); this.showed = true; this.container = UI.createEl("vot-block", ["vot-tooltip"], this.content); this.syncContentClass(); if (this._bordered) this.container.classList.add("vot-tooltip-bordered"); this.container.setAttribute("role", "tooltip"); this.container.id = this.tooltipId; this.container.dataset.trigger = this.trigger; this.container.dataset.mode = this.mode; this.container.dataset.position = this.position; this.container.style.position = this.usesPortalCoordinates() ? "absolute" : "fixed"; this.container.style.top = "0"; this.container.style.left = "0"; this.container.style.margin = "0"; this.portal.appendChild(this.container); if (this.backgroundColor) this.container.style.backgroundColor = this.backgroundColor; if (this.borderRadius !== void 0) this.container.style.borderRadius = `${this.borderRadius}px`; if (this._hidden) this.container.hidden = true; else this.syncAriaDescribedBy(true); this.container.style.opacity = "1"; if (this.trigger === "hover") this.container.addEventListener("pointerleave", this.onTooltipPointerLeave); else document.addEventListener("pointerdown", this.onDocumentPointerDown, { capture: true, passive: true }); this.attachScrollListener(); this.resizeObserver?.observe(this.anchor); if (this.edgeAnchor !== this.anchor) this.resizeObserver?.observe(this.edgeAnchor); this.intersectionObserver?.observe(this.target); this.updatePos(); return this; } updatePos() { if (!this.container) return this; const viewportWidth = window.innerWidth; const availableWidth = Math.max(0, viewportWidth - this.offsetX * 2); const maxWidth = clamp(this.maxWidth ?? availableWidth, 0, availableWidth); this.container.style.maxWidth = `${maxWidth}px`; const { top, left } = this.computePosition(this.autoLayout, this.preferredPosition); const offset = this.getPortalViewportOffset(); this.container.style.transform = `translate(${left - offset.left}px, ${top - offset.top}px)`; this.container.dataset.position = this.position; return this; } usesPortalCoordinates() { if (this.portal instanceof ShadowRoot) return true; if (this.portal === document.body || this.portal === document.documentElement) return false; if (this.portal.classList.contains("vot-portal")) return false; return true; } getPortalViewportOffset() { if (!this.usesPortalCoordinates()) return { top: 0, left: 0 }; const rect = (this.portal instanceof ShadowRoot ? this.portal.host : this.portal).getBoundingClientRect(); return { top: rect.top, left: rect.left }; } computePosition(autoLayout, preferred) { const anchor = this.getAnchorBox(); const tooltipRect = this.container?.getBoundingClientRect(); const tooltip = { width: tooltipRect.width || 100, height: tooltipRect.height || 40 }; const viewport = this.getPositionBoundary(); const position = autoLayout ? this.resolvePosition(anchor, tooltip, viewport, preferred) : preferred; const coords = this.getCoordinates(anchor, tooltip, position); this.position = position; return { top: clamp(coords.top, viewport.top, viewport.bottom - tooltip.height), left: clamp(coords.left, viewport.left, viewport.right - tooltip.width) }; } getAnchorBox() { const anchorRect = this.anchor.getBoundingClientRect(); const edgeRect = this.edgeAnchor.getBoundingClientRect(); return { left: edgeRect.left, right: edgeRect.right, top: edgeRect.top, bottom: edgeRect.bottom, centerX: anchorRect.left + anchorRect.width / 2, centerY: anchorRect.top + anchorRect.height / 2 }; } getPositionBoundary() { const fallback = { left: this.offsetX, right: window.innerWidth - this.offsetX, top: this.offsetY, bottom: window.innerHeight - this.offsetY, width: window.innerWidth, height: window.innerHeight }; if (this.mode !== "follow" || !this.usesPortalCoordinates()) return fallback; const rect = (this.portal instanceof ShadowRoot ? this.portal.host : this.portal).getBoundingClientRect(); if (!rect.width || !rect.height) return fallback; return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }; } resolvePosition(anchor, tooltip, viewport, preferred) { if (this.mode === "follow") return this.resolveFollowPosition(anchor, tooltip, viewport, preferred); switch (preferred) { case "top": return anchor.top - viewport.top >= tooltip.height + this.offsetY ? "top" : "bottom"; case "bottom": return viewport.bottom - anchor.bottom >= tooltip.height + this.offsetY ? "bottom" : "top"; case "left": return anchor.left - viewport.left >= tooltip.width + this.offsetX ? "left" : "right"; case "right": return viewport.right - anchor.right >= tooltip.width + this.offsetX ? "right" : "left"; } } resolveFollowPosition(anchor, tooltip, viewport, preferred) { if (preferred === "top" || preferred === "bottom") { const topWouldClamp = anchor.top - tooltip.height - this.offsetY < viewport.top; const bottomWouldClamp = anchor.bottom + this.offsetY + tooltip.height > viewport.bottom; if (preferred === "top") { if (!topWouldClamp || bottomWouldClamp) return "top"; return "bottom"; } if (!bottomWouldClamp || topWouldClamp) return "bottom"; return "top"; } const leftWouldClamp = anchor.left - tooltip.width - this.offsetX < viewport.left; const rightWouldClamp = anchor.right + this.offsetX + tooltip.width > viewport.right; if (preferred === "left") { if (!leftWouldClamp || rightWouldClamp) return "left"; return "right"; } if (!rightWouldClamp || leftWouldClamp) return "right"; return "left"; } getCoordinates(anchor, tooltip, position) { switch (position) { case "top": return { top: anchor.top - tooltip.height - this.offsetY, left: anchor.centerX - tooltip.width / 2 }; case "bottom": return { top: anchor.bottom + this.offsetY, left: anchor.centerX - tooltip.width / 2 }; case "left": return { top: anchor.centerY - tooltip.height / 2, left: anchor.left - tooltip.width - this.offsetX }; case "right": return { top: anchor.centerY - tooltip.height / 2, left: anchor.right + this.offsetX }; } } destroy(instant = false) { if (!this.container) return this; const container = this.container; this.cancelPositionUpdate(); this.clearDestroyFallbackTimer(); this.showed = false; this.syncAriaDescribedBy(false); this.resizeObserver?.disconnect(); this.intersectionObserver?.disconnect(); this.detachScrollListener(); this.detachOutsidePointerListener(); if (instant) { container.remove(); this.container = void 0; return this; } container.removeEventListener("pointerleave", this.onTooltipPointerLeave); container.style.pointerEvents = "none"; container.style.opacity = "0"; const handleTransitionDone = () => { this.clearDestroyFallbackTimer(); container?.remove(); if (this.container === container) this.container = void 0; }; container.addEventListener("transitionend", handleTransitionDone, { once: true }); container.addEventListener("transitioncancel", handleTransitionDone, { once: true }); this.destroyFallbackTimerId = globalThis.setTimeout(handleTransitionDone, Tooltip.DESTROY_FALLBACK_MS); return this; } detachOutsidePointerListener() { document.removeEventListener("pointerdown", this.onDocumentPointerDown, { capture: true }); } syncAriaDescribedBy(isShowing) { const existing = this.target.getAttribute("aria-describedby"); this.prevAriaDescribedBy ??= existing; if (!isShowing) { if (this.prevAriaDescribedBy === null) this.target.removeAttribute("aria-describedby"); else this.target.setAttribute("aria-describedby", this.prevAriaDescribedBy); this.prevAriaDescribedBy = null; return; } const tokens = new Set((existing ?? "").split(/\s+/).filter(Boolean)); tokens.add(this.tooltipId); this.target.setAttribute("aria-describedby", Array.from(tokens).join(" ")); } set bordered(value) { this._bordered = value; this.container?.classList.toggle("vot-tooltip-bordered", value); } get bordered() { return this._bordered; } set hidden(value) { this._hidden = value; if (this.container) this.container.hidden = value; if (this.showed) this.syncAriaDescribedBy(!value); } get hidden() { return this._hidden; } attachScrollListener() { if (this.scrollListening) return; this.scrollListening = true; document.addEventListener("scroll", this.onScroll, { passive: true, capture: true }); } detachScrollListener() { if (!this.scrollListening) return; this.scrollListening = false; document.removeEventListener("scroll", this.onScroll, { capture: true }); } }; //#endregion //#region src/ui/shadowMount.ts var shadowScopedCssText = scopeCssForShadowRoots(".vot-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));--vot-helper-ontheme:var(--vot-ontheme-rgb,var(--vot-onprimary-rgb,255, 255, 255));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;min-width:64px;height:36px;color:rgb(var(--vot-helper-ontheme));background-color:rgb(var(--vot-helper-theme));box-shadow:var(--vot-shadow-1);transition:box-shadow var(--vot-duration-medium) var(--vot-easing-standard);outline:none;font-size:14px;line-height:36px;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-4)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:none!important;font-weight:500!important}.vot-button:before,.vot-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-button:before{background-color:rgb(var(--vot-helper-ontheme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-button:hover:before{opacity:.08}.vot-button:active:after{opacity:.32;background-size:100% 100%;transition:background-size}.vot-button:hover,.vot-button:active{box-shadow:var(--vot-shadow-2)}.vot-button[disabled=true]{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .12);color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);box-shadow:none;cursor:initial}.vot-button[disabled=true]:before,.vot-button[disabled=true]:after{opacity:0}.vot-outlined-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;min-width:64px;height:36px;color:rgb(var(--vot-helper-theme));background-color:#0000;outline:none;font-size:14px;line-height:34px;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-4)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:solid 1px var(--vot-border-color)!important;margin:0!important;font-weight:500!important}.vot-outlined-button:before,.vot-outlined-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-outlined-button:before{background-color:rgb(var(--vot-helper-theme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-outlined-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-outlined-button:hover:before{opacity:.04}.vot-outlined-button:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-outlined-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-outlined-button[disabled=true]:before,.vot-outlined-button[disabled=true]:after{opacity:0}.vot-text-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;min-width:64px;height:36px;color:rgb(var(--vot-helper-theme));background-color:#0000;outline:none;font-size:14px;line-height:36px;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:none!important;margin:0!important;font-weight:500!important}.vot-text-button:before,.vot-text-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-text-button:before{background-color:rgb(var(--vot-helper-theme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-text-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-text-button:hover:before{opacity:.04}.vot-text-button:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-text-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-text-button[disabled=true]:before,.vot-text-button[disabled=true]:after{opacity:0}.vot-icon-button{--vot-helper-onsurface:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87);box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;width:36px;min-width:36px;height:36px;fill:var(--vot-helper-onsurface);color:var(--vot-helper-onsurface);background-color:#0000;outline:none;font-size:14px;line-height:36px;display:inline-block;position:relative;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:none!important;border-radius:50%!important;margin:0!important;padding:0!important;font-weight:500!important}.vot-icon-button:before,.vot-icon-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-icon-button:before{background-color:var(--vot-helper-onsurface);transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-icon-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-icon-button:hover:before{opacity:.04}.vot-icon-button:active:after{opacity:.32;background-size:100% 100%;transition:background-size}.vot-icon-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);fill:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-icon-button[disabled=true]:before,.vot-icon-button[disabled=true]:after{opacity:0}.vot-icon-button svg{fill:inherit;stroke:inherit;width:24px;height:36px}.vot-hotkey{justify-content:flex-start;align-items:center;gap:var(--vot-space-3,12px);flex-wrap:wrap;display:flex}.vot-hotkey-label{overflow-wrap:anywhere;max-width:80%}.vot-hotkey-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;background-color:#0000;outline:none;width:fit-content;min-width:32px;height:fit-content;font-size:15px;line-height:1.5;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:solid 1px var(--vot-border-color)!important;margin:0!important;font-weight:400!important}.vot-hotkey-button:before,.vot-hotkey-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-hotkey-button:before{background-color:rgb(var(--vot-helper-theme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-hotkey-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-hotkey-button:hover:before{opacity:.04}.vot-hotkey-button:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-hotkey-button[data-status=active]{color:rgb(var(--vot-helper-theme))}.vot-hotkey-button[data-status=active]:before{opacity:.04}.vot-hotkey-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-hotkey-button[disabled=true]:before,.vot-hotkey-button[disabled=true]:after{opacity:0}.vot-textfield{display:inline-block;--vot-helper-theme:rgb(var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243)))!important;--vot-helper-safari1:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important;--vot-helper-safari2:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6)!important;--vot-helper-safari3:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;text-align:start!important;padding-top:6px!important;font-size:16px!important;line-height:1.5!important;position:relative!important}.vot-textfield>:is(input,textarea){box-sizing:border-box!important;border-style:solid!important;border-width:1px!important;border-color:transparent var(--vot-helper-safari2) var(--vot-helper-safari2)!important;width:100%!important;height:inherit!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important;-webkit-text-fill-color:currentColor!important;font-family:inherit!important;font-size:inherit!important;line-height:inherit!important;caret-color:var(--vot-helper-theme)!important;background-color:#0000!important;border-radius:4px!important;margin:0!important;padding:15px 13px!important;transition:border .2s,box-shadow .2s!important;box-shadow:inset 1px 0 #0000,inset -1px 0 #0000,inset 0 -1px #0000!important}.vot-textfield>:is(input,textarea):not(:focus):not(:is(.vot-show-placeholder,.vot-show-placeholer))::placeholder{color:#0000!important}.vot-textfield>:is(input,textarea):not(:focus):placeholder-shown{border-top-color:var(--vot-helper-safari2)!important}.vot-textfield>:is(input,textarea)+span{font-family:inherit;width:100%!important;max-height:100%!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6)!important;cursor:text!important;pointer-events:none!important;font-size:75%!important;line-height:15px!important;transition:color .2s,font-size .2s,line-height .2s!important;display:flex!important;position:absolute!important;top:0!important;left:0!important}.vot-textfield>:is(input,textarea):not(:focus):placeholder-shown+span{font-size:inherit!important;line-height:68px!important}.vot-textfield>input+span:before,.vot-textfield>input+span:after,.vot-textfield>textarea+span:before,.vot-textfield>textarea+span:after{content:\"\"!important;box-sizing:border-box!important;border-top:solid 1px var(--vot-helper-safari2)!important;pointer-events:none!important;min-width:10px!important;height:8px!important;margin-top:6px!important;transition:border .2s,box-shadow .2s!important;display:block!important;box-shadow:inset 0 1px #0000!important}.vot-textfield>input+span:before,.vot-textfield>textarea+span:before{border-left:1px solid #0000!important;border-radius:4px 0!important;margin-right:4px!important}.vot-textfield>input+span:after,.vot-textfield>textarea+span:after{border-right:1px solid #0000!important;border-radius:0 4px!important;flex-grow:1!important;margin-left:4px!important}.vot-textfield>input:is(.vot-show-placeholder,.vot-show-placeholer)+span:before,.vot-textfield>textarea:is(.vot-show-placeholder,.vot-show-placeholer)+span:before{margin-right:0!important}.vot-textfield>input:is(.vot-show-placeholder,.vot-show-placeholer)+span:after,.vot-textfield>textarea:is(.vot-show-placeholder,.vot-show-placeholer)+span:after{margin-left:0!important}.vot-textfield>input:not(:focus):placeholder-shown+span:before,.vot-textfield>input:not(:focus):placeholder-shown+span:after,.vot-textfield>textarea:not(:focus):placeholder-shown+span:before,.vot-textfield>textarea:not(:focus):placeholder-shown+span:after{border-top-color:#0000!important}.vot-textfield:hover>input:not(:disabled),.vot-textfield:hover>textarea:not(:disabled){border-color:transparent var(--vot-helper-safari3) var(--vot-helper-safari3)!important}.vot-textfield:hover>input:not(:disabled)+span:before,.vot-textfield:hover>input:not(:disabled)+span:after,.vot-textfield:hover>textarea:not(:disabled)+span:before,.vot-textfield:hover>textarea:not(:disabled)+span:after{border-top-color:var(--vot-helper-safari3)!important}.vot-textfield:hover>input:not(:disabled):not(:focus):placeholder-shown,.vot-textfield:hover>textarea:not(:disabled):not(:focus):placeholder-shown{border-color:var(--vot-helper-safari3)!important}.vot-textfield>input:focus,.vot-textfield>textarea:focus{border-color:transparent var(--vot-helper-theme) var(--vot-helper-theme)!important;box-shadow:inset 1px 0 var(--vot-helper-theme), inset -1px 0 var(--vot-helper-theme), inset 0 -1px var(--vot-helper-theme)!important;outline:none!important}.vot-textfield>input:focus+span,.vot-textfield>textarea:focus+span{color:var(--vot-helper-theme)!important}.vot-textfield>input:focus+span:before,.vot-textfield>input:focus+span:after,.vot-textfield>textarea:focus+span:before,.vot-textfield>textarea:focus+span:after{border-top-color:var(--vot-helper-theme)!important;box-shadow:inset 0 1px var(--vot-helper-theme)!important}.vot-textfield>input:disabled,.vot-textfield>input:disabled+span,.vot-textfield>textarea:disabled,.vot-textfield>textarea:disabled+span{border-color:transparent var(--vot-helper-safari1) var(--vot-helper-safari1)!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important;pointer-events:none!important}.vot-textfield>input:disabled+span:before,.vot-textfield>input:disabled+span:after,.vot-textfield>textarea:disabled+span:before,.vot-textfield>textarea:disabled+span:after,.vot-textfield>input:disabled:placeholder-shown,.vot-textfield>input:disabled:placeholder-shown+span,.vot-textfield>textarea:disabled:placeholder-shown,.vot-textfield>textarea:disabled:placeholder-shown+span{border-top-color:var(--vot-helper-safari1)!important}.vot-textfield>input:disabled:placeholder-shown+span:before,.vot-textfield>input:disabled:placeholder-shown+span:after,.vot-textfield>textarea:disabled:placeholder-shown+span:before,.vot-textfield>textarea:disabled:placeholder-shown+span:after{border-top-color:#0000!important}@media not all and (resolution>=.001dpcm){@supports ((-webkit-appearance:none)){.vot-textfield>input,.vot-textfield>input+span,.vot-textfield>textarea,.vot-textfield>textarea+span,.vot-textfield>input+span:before,.vot-textfield>input+span:after,.vot-textfield>textarea+span:before,.vot-textfield>textarea+span:after{transition-duration:.1s!important}}}.vot-checkbox{--vot-checkbox-label-offset:30px;--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));--vot-helper-ontheme:var(--vot-ontheme-rgb,var(--vot-onprimary-rgb,255, 255, 255));z-index:0;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87);text-align:start;font-size:16px;line-height:1.5;display:inline-block;position:relative;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;text-transform:none!important}.vot-checkbox-sub{padding-left:var(--vot-checkbox-label-offset)!important}.vot-checkbox>input{appearance:none;z-index:10000;box-sizing:border-box;opacity:1;cursor:pointer;background:0 0;outline:none;width:18px;height:18px;transition:border-color .2s,background-color .2s;display:block;position:absolute;border:2px solid!important;border-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6)!important;border-radius:2px!important;margin:3px 1px!important;padding:0!important}.vot-checkbox>input+span{box-sizing:border-box;width:inherit;cursor:pointer;font-family:inherit;display:inline-block;position:relative;padding-left:var(--vot-checkbox-label-offset)!important;font-weight:400!important}.vot-checkbox>input+span:before{content:\"\";background-color:rgb(var(--vot-onsurface-rgb,0, 0, 0));opacity:0;pointer-events:none;width:40px;height:40px;transition:opacity .3s,transform .2s;display:block;position:absolute;top:-8px;left:-10px;transform:scale(1);border-radius:50%!important}.vot-checkbox>input+span:after{content:\"\";z-index:10000;pointer-events:none;width:10px;height:5px;transition:border-color .2s;display:block;position:absolute;top:3px;left:1px;transform:translate(3px,4px)rotate(-45deg);box-sizing:content-box!important;border:0 solid #0000!important;border-width:0 0 2px 2px!important}.vot-checkbox>input:checked,.vot-checkbox>input:indeterminate{background-color:rgb(var(--vot-helper-theme));border-color:rgb(var(--vot-helper-theme))!important}.vot-checkbox>input:checked+span:before,.vot-checkbox>input:indeterminate+span:before{background-color:rgb(var(--vot-helper-theme))}.vot-checkbox>input:checked+span:after,.vot-checkbox>input:indeterminate+span:after{border-color:rgb(var(--vot-helper-ontheme,255, 255, 255))!important}.vot-checkbox>input:hover{box-shadow:none!important}.vot-checkbox>input:indeterminate+span:after{transform:translate(4px,3px);border-left-width:0!important}.vot-checkbox:hover>input+span:before{opacity:.04}.vot-checkbox:active>input,.vot-checkbox:active:hover>input:not(:disabled){border-color:rgb(var(--vot-helper-theme))!important}.vot-checkbox:active>input:checked{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6);border-color:#0000!important}.vot-checkbox:active>input+span:before{opacity:1;transition:transform,opacity;transform:scale(0)}.vot-checkbox>input:disabled{cursor:initial;border-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-checkbox>input:disabled:checked,.vot-checkbox>input:disabled:indeterminate{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);border-color:#0000!important}.vot-checkbox>input:disabled+span{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial}.vot-checkbox>input:disabled+span:before{opacity:0;transform:scale(0)}html.vot-keyboard-nav .vot-checkbox>input:focus-visible{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}@supports not selector(:focus-visible){html.vot-keyboard-nav .vot-checkbox>input:focus{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}}.vot-slider{flex-direction:column;gap:6px;display:flex;width:100%!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", BlinkMacSystemFont, system-ui, -apple-system)!important;text-align:start!important;font-size:16px!important;line-height:1.5!important}.vot-slider>span{order:1;margin:0!important;display:block!important}.vot-slider .vot-slider-label{flex-wrap:wrap;align-items:baseline;gap:6px;width:100%;display:inline-flex}.vot-slider-label-value{font-variant-numeric:tabular-nums;margin-left:0!important;font-weight:500!important}.vot-slider .vot-slider-label-text{min-width:0}.vot-slider>input{order:2;appearance:none!important;cursor:pointer!important;background-color:#0000!important;border:none!important;width:100%!important;height:32px!important;margin:0!important;padding:0!important;display:block!important;position:relative!important;top:0!important}.vot-slider>input:hover{box-shadow:none!important}.vot-slider>input:before{content:\"\"!important;width:calc(100% * var(--vot-progress,0))!important;background:rgb(var(--vot-primary-rgb,33, 150, 243))!important;height:2px!important;display:block!important;position:absolute!important;top:calc(50% - 1px)!important}.vot-slider>input:disabled{cursor:default!important;opacity:.38!important}.vot-slider>input:disabled+span{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-slider>input:disabled::-webkit-slider-runnable-track{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-slider>input:disabled::-moz-range-track{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-slider>input:disabled::-webkit-slider-thumb{background-color:rgb(var(--vot-onsurface-rgb,0, 0, 0))!important;box-shadow:0 0 0 1px rgb(var(--vot-surface-rgb,255, 255, 255))!important;transform:scale(4)!important}.vot-slider>input:disabled::-moz-range-thumb{background-color:rgb(var(--vot-onsurface-rgb,0, 0, 0))!important;box-shadow:0 0 0 1px rgb(var(--vot-surface-rgb,255, 255, 255))!important;transform:scale(4)!important}.vot-slider>input:disabled::-moz-range-progress{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important}.vot-slider>input:focus{outline:none!important}.vot-slider>input::-webkit-slider-runnable-track{background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important;border-radius:1px!important;width:100%!important;height:2px!important;margin:15px 0!important}.vot-slider>input::-moz-range-track{background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important;border-radius:1px!important;width:100%!important;height:2px!important;margin:15px 0!important}.vot-slider>input::-webkit-slider-thumb{appearance:none!important;background-color:rgb(var(--vot-primary-rgb,33, 150, 243))!important;width:2px!important;height:2px!important;box-shadow:none!important;border:none!important;border-radius:50%!important;transition:box-shadow .2s!important;transform:scale(6)!important}.vot-slider>input::-moz-range-thumb{appearance:none!important;background-color:rgb(var(--vot-primary-rgb,33, 150, 243))!important;width:2px!important;height:2px!important;box-shadow:none!important;border:none!important;border-radius:50%!important;transition:box-shadow .2s!important;transform:scale(6)!important}.vot-slider>input::-webkit-slider-thumb{-webkit-appearance:none!important;margin:0!important}.vot-slider>input::-moz-range-progress{background-color:rgb(var(--vot-primary-rgb,33, 150, 243))!important;border-radius:1px!important;height:2px!important}.vot-slider>input:focus:not(:focus-visible)::-webkit-slider-thumb{box-shadow:none!important}.vot-slider>input:focus:not(:focus-visible)::-moz-range-thumb{box-shadow:none!important}html.vot-keyboard-nav .vot-slider>input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}html.vot-keyboard-nav .vot-slider>input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}@supports not selector(:focus-visible){html.vot-keyboard-nav .vot-slider>input:focus::-webkit-slider-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}html.vot-keyboard-nav .vot-slider>input:focus::-moz-range-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}}.vot-select{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);--vot-helper-safari1:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6);--vot-helper-safari2:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;color:var(--vot-helper-theme);fill:var(--vot-helper-theme);justify-content:space-between;align-items:center;font-size:14px;line-height:1.5;display:flex;font-weight:400!important}.vot-select-outer{cursor:pointer;justify-content:space-between;align-items:center;width:120px;max-width:120px;display:flex;border:1px solid var(--vot-helper-safari1)!important;border-radius:4px!important;padding:0 5px!important;transition:border .2s!important}.vot-select-outer:hover{border-color:var(--vot-helper-safari2)!important}.vot-select-outer[disabled=true]{opacity:.5;cursor:default}.vot-select-outer[disabled=true]:hover{border-color:var(--vot-helper-safari1)!important}.vot-select-title{text-overflow:ellipsis;white-space:nowrap;font-family:inherit;overflow:hidden}.vot-select-arrow-icon{justify-content:center;align-items:center;width:20px;height:32px;display:flex}.vot-select-arrow-icon svg{fill:inherit;stroke:inherit}.vot-select-content-list{flex-direction:column;display:flex}.vot-select-content-list .vot-select-content-item{cursor:pointer;border-radius:8px!important;padding:5px 10px!important}.vot-select-content-list .vot-select-content-item:not([inert]):hover{background-color:#2a2c31}.vot-select-content-list .vot-select-content-item[data-vot-selected=true]{color:rgb(var(--vot-primary-rgb,33, 150, 243));background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .2)}.vot-select-content-list .vot-select-content-item[data-vot-selected=true]:hover{background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .1)!important}.vot-select-content-list .vot-select-content-item[inert]{cursor:default;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)}.vot-header{color:rgba(var(--vot-helper-onsurface-rgb), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;line-height:1.5;font-weight:700!important}.vot-header:not(:first-child){padding-top:8px}.vot-header-level-1{font-size:2em}.vot-header-level-2{font-size:1.5em}.vot-header-level-3{font-size:1.17em}.vot-header-level-4{font-size:1em}.vot-header-level-5{font-size:.83em}.vot-header-level-6{font-size:.67em}.vot-info{color:rgba(var(--vot-helper-onsurface-rgb), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;-webkit-user-select:text;user-select:text;font-size:16px;line-height:1.5;display:flex}.vot-info>:not(:first-child){color:rgba(var(--vot-helper-onsurface-rgb), .5);flex:1;margin-left:8px!important}.vot-details{color:rgba(var(--vot-helper-onsurface-rgb), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;cursor:pointer;transition:background var(--vot-duration-medium) var(--vot-easing-standard);justify-content:space-between;align-items:center;font-size:16px;line-height:1.5;display:flex;border-radius:.5em!important;margin:-.5em!important;padding:.5em!important}.vot-details-arrow-icon{width:20px;height:32px;fill:rgba(var(--vot-helper-onsurface-rgb), .87);justify-content:center;align-items:center;display:flex;transform:scale(1.25)rotate(-90deg)}.vot-details:hover{background:rgba(var(--vot-onsurface-rgb,0, 0, 0), .06)}.vot-settings-section{border:1px solid var(--vot-border-color);border-radius:var(--vot-radius-l);padding:var(--vot-space-2);background:rgba(var(--vot-helper-onsurface-rgb), .03);flex-direction:column;display:flex}.vot-settings-section>*{margin:0!important}.vot-settings-section>*+*{margin-top:var(--vot-space-2)!important}.vot-settings-section-header{border-radius:var(--vot-radius-m);margin:0!important;padding:.45em .5em!important}.vot-settings-section-header .vot-details-arrow-icon{transition:transform var(--vot-duration-medium) var(--vot-easing-standard)}.vot-settings-section-header[data-open=true] .vot-details-arrow-icon{transform:scale(1.25)rotate(0)}.vot-settings-section-content{--vot-settings-control-width:200px;--vot-settings-row-gap:var(--vot-space-2);padding:0 var(--vot-space-1) var(--vot-space-1);flex-direction:column;display:flex}.vot-settings-section-content>*{margin:0!important}.vot-settings-section-content>*+*{margin-top:var(--vot-settings-row-gap)!important}.vot-settings-section-content>.vot-checkbox,.vot-settings-section-content>.vot-hotkey,.vot-settings-section-content>.vot-textfield,.vot-settings-section-content>.vot-select,.vot-settings-section-content>.vot-slider{padding:var(--vot-space-1);box-sizing:border-box;width:100%!important}.vot-settings-section-content>.vot-textfield{gap:var(--vot-space-1);flex-direction:column;padding-top:0!important;display:flex!important}.vot-settings-section-content>.vot-textfield>span{order:0;width:auto!important;max-height:none!important;color:rgba(var(--vot-helper-onsurface-rgb), .72)!important;cursor:default!important;pointer-events:none!important;font-size:13px!important;line-height:1.2!important;display:block!important;position:static!important}.vot-settings-section-content>.vot-textfield>span:before,.vot-settings-section-content>.vot-textfield>span:after{content:none!important;display:none!important}.vot-settings-section-content>.vot-textfield>input,.vot-settings-section-content>.vot-textfield>textarea{transition:border-color var(--vot-duration-fast) var(--vot-easing-standard), background-color var(--vot-duration-fast) var(--vot-easing-standard);order:1;width:100%!important;height:36px!important;padding:0 var(--vot-space-3)!important;border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-s)!important;background:rgba(var(--vot-helper-onsurface-rgb), .04)!important;color:rgba(var(--vot-helper-onsurface-rgb), .9)!important;-webkit-text-fill-color:currentColor!important;box-shadow:none!important}.vot-settings-section-content>.vot-textfield>textarea{resize:vertical;height:auto!important;min-height:84px!important;padding:var(--vot-space-2) var(--vot-space-3)!important}.vot-settings-section-content>.vot-textfield>input::placeholder,.vot-settings-section-content>.vot-textfield>textarea::placeholder{color:rgba(var(--vot-helper-onsurface-rgb), .55)!important}.vot-settings-section-content>.vot-textfield:hover>input,.vot-settings-section-content>.vot-textfield:hover>textarea{border-color:var(--vot-border-color-hover)!important}.vot-settings-section-content>.vot-textfield>input:not(:focus):placeholder-shown,.vot-settings-section-content>.vot-textfield>textarea:not(:focus):placeholder-shown{border-color:var(--vot-border-color)!important}.vot-settings-section-content>.vot-textfield>input:focus,.vot-settings-section-content>.vot-textfield>textarea:focus{border-color:rgba(var(--vot-primary-rgb), .7)!important}.vot-lang-select{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);color:var(--vot-helper-theme);fill:var(--vot-helper-theme);justify-content:space-between;align-items:center;display:flex}.vot-lang-select-icon{justify-content:center;align-items:center;width:32px;height:32px;display:flex}.vot-lang-select-icon svg{fill:inherit;stroke:inherit}.vot-segmented-button{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);--vot-button-default-top:5rem;--vot-button-top-offset:max(16px, env(safe-area-inset-top,0px));--vot-button-side-offset:max(16px, env(safe-area-inset-left,0px));--vot-button-side-offset-right:max(16px, env(safe-area-inset-right,0px));--vot-button-side-top-offset:max(clamp(48px, 12.5vh, 128px), env(safe-area-inset-top,0px));left:50%;right:auto;top:var(--vot-button-default-top);position:absolute;overflow:hidden;transform:translate(-50%);opacity:1!important;pointer-events:auto!important;touch-action:none!important}@media (pointer:coarse){.vot-segmented-button{--vot-button-default-top:3rem;--vot-button-top-offset:max(12px, env(safe-area-inset-top,0px));--vot-button-side-offset:max(10px, env(safe-area-inset-left,0px));--vot-button-side-offset-right:max(10px, env(safe-area-inset-right,0px));--vot-button-side-top-offset:max(clamp(42px, 12.5vh, 112px), env(safe-area-inset-top,0px))}}.vot-segmented-button{-webkit-user-select:none;user-select:none;background:rgb(var(--vot-surface-rgb,255, 255, 255));max-width:100vw;height:36px;color:var(--vot-helper-theme);fill:var(--vot-helper-theme);cursor:default;transition:opacity var(--vot-duration-slow) var(--vot-easing-standard);z-index:2147483647;align-items:center;font-size:16px;line-height:1.5;display:flex;border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-s)!important;box-shadow:var(--vot-shadow-1)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important}.vot-segmented-button.vot-segmented-button--hidden{opacity:0!important;pointer-events:none!important}.vot-segmented-button.vot-segmented-button--dragging{cursor:grabbing;will-change:transform;transform:translate3d(var(--vot-button-drag-left,0px), var(--vot-button-drag-top,0px), 0)!important;opacity:.96!important;box-shadow:var(--vot-shadow-1)!important;transition:none!important;top:0!important;left:0!important;right:auto!important}.vot-segmented-button.vot-segmented-button--dock-preview{z-index:2147483646;transition:left var(--vot-duration-medium) var(--vot-easing-standard), right var(--vot-duration-medium) var(--vot-easing-standard), top var(--vot-duration-medium) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard), opacity var(--vot-duration-fast) var(--vot-easing-standard);opacity:.72!important;pointer-events:none!important;border-color:var(--vot-border-color)!important;box-shadow:var(--vot-shadow-1)!important}.vot-segmented-button *{box-sizing:border-box!important}.vot-segmented-button .vot-separator{background:rgba(var(--vot-helper-theme-rgb), .1);width:1px;height:50%}.vot-segmented-button .vot-segment,.vot-segmented-button .vot-segment-only-icon{height:100%;color:inherit;transition:background-color var(--vot-duration-fast) var(--vot-easing-standard);-webkit-tap-highlight-color:transparent;background-color:#0000;outline:none;justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden;padding:0 var(--vot-space-2)!important;border:none!important}.vot-segmented-button .vot-segment:focus,.vot-segmented-button .vot-segment-only-icon:focus{box-shadow:inset 0 0 0 2px var(--vot-focus-ring-color);outline:none}.vot-segmented-button .vot-segment:focus:not(:focus-visible),.vot-segmented-button .vot-segment-only-icon:focus:not(:focus-visible){box-shadow:none}.vot-segmented-button .vot-segment:before,.vot-segmented-button .vot-segment-only-icon:before,.vot-segmented-button .vot-segment:after,.vot-segmented-button .vot-segment-only-icon:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-segmented-button .vot-segment:before,.vot-segmented-button .vot-segment-only-icon:before{background-color:rgb(var(--vot-helper-theme-rgb));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-segmented-button .vot-segment:after,.vot-segmented-button .vot-segment-only-icon:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-segmented-button .vot-segment:hover:before,.vot-segmented-button .vot-segment-only-icon:hover:before{opacity:.04}.vot-segmented-button .vot-segment:active:after,.vot-segmented-button .vot-segment-only-icon:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-segmented-button .vot-segment:before,.vot-segmented-button .vot-segment-only-icon:before,.vot-segmented-button .vot-segment:after,.vot-segmented-button .vot-segment-only-icon:after{pointer-events:none}.vot-segmented-button .vot-segment>svg,.vot-segmented-button .vot-segment-only-icon>svg,.vot-segmented-button .vot-segment-label,.vot-segmented-button .vot-segment .vot-dropdown-arrow,.vot-segmented-button .vot-segment-only-icon .vot-dropdown-arrow{z-index:1;position:relative}.vot-segmented-button .vot-segment-only-icon{min-width:36px;padding:0!important}.vot-segmented-button .vot-segment-label{white-space:nowrap;color:inherit;margin-left:var(--vot-space-2)!important;font-weight:400!important}.vot-segmented-button .vot-translate-button{gap:0!important;padding-inline-end:4px!important}.vot-segmented-button .vot-dropdown-arrow{width:30px;min-width:30px;height:100%;color:inherit;fill:inherit;opacity:.95;cursor:default;-webkit-tap-highlight-color:transparent;border-radius:999px;outline:none;flex:none;justify-content:center;align-items:center;display:inline-flex;position:relative;margin-inline:4px -4px!important}.vot-segmented-button .vot-dropdown-arrow:focus{box-shadow:inset 0 0 0 2px var(--vot-focus-ring-color)}.vot-segmented-button .vot-dropdown-arrow:focus:not(:focus-visible){box-shadow:none}.vot-segmented-button .vot-dropdown-arrow[hidden]{display:none!important}.vot-segmented-button .vot-dropdown-arrow:before,.vot-segmented-button .vot-dropdown-arrow:after{display:none}.vot-segmented-button .vot-dropdown-arrow svg{transform-origin:50%;width:28px;height:28px;transition:transform var(--vot-duration-fast) var(--vot-easing-standard);transform:scale(1.08)}.vot-segmented-button .vot-dropdown-arrow[aria-expanded=true] svg,.vot-segmented-button .vot-dropdown-arrow.vot-dropdown-arrow--open svg{transform:rotate(180deg)scale(1.08)}.vot-segmented-button .vot-subtitles-button[data-active=true],.vot-segmented-button[data-status=success] .vot-dropdown-arrow{color:rgb(var(--vot-primary-rgb,33, 150, 243));fill:rgb(var(--vot-primary-rgb,33, 150, 243))}.vot-segmented-button[data-status=error] .vot-dropdown-arrow{color:#f28b82;fill:#f28b82}.vot-segmented-button[data-status=success] .vot-translate-button{color:rgb(var(--vot-primary-rgb,33, 150, 243));fill:rgb(var(--vot-primary-rgb,33, 150, 243))}.vot-segmented-button[data-status=error] .vot-translate-button{color:#f28b82;fill:#f28b82}.vot-segmented-button[data-loading=true] #vot-loading-icon{display:block!important}.vot-segmented-button[data-loading=true] #vot-translate-icon{display:none!important}.vot-segmented-button[data-direction=column]{flex-direction:column;height:fit-content}.vot-segmented-button[data-direction=column] .vot-segment-label,.vot-segmented-button[data-direction=column] .vot-dropdown-arrow{display:none}.vot-segmented-button[data-direction=column]>.vot-segment-only-icon,.vot-segmented-button[data-direction=column]>.vot-segment{padding:8px!important}.vot-segmented-button[data-direction=column] .vot-separator{width:50%;height:1px}.vot-segmented-button[data-position=left]{left:var(--vot-button-side-offset);right:auto;top:var(--vot-button-side-top-offset);transform:none}.vot-segmented-button[data-position=right]{left:auto;right:var(--vot-button-side-offset-right);top:var(--vot-button-side-top-offset);transform:none}.vot-segmented-button[data-position=leftCenter]{left:var(--vot-button-side-offset);top:50%;right:auto;transform:translateY(-50%)}.vot-segmented-button[data-position=rightCenter]{left:auto;right:var(--vot-button-side-offset-right);top:50%;transform:translateY(-50%)}.vot-segmented-button svg{width:24px;fill:inherit;stroke:inherit}.vot-tooltip{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);--vot-helper-ondialog:rgb(var(--vot-ondialog-rgb,37, 38, 40));--vot-helper-border:rgb(var(--vot-tooltip-border,69, 69, 69));-webkit-user-select:none;user-select:none;background:rgb(var(--vot-surface-rgb,255, 255, 255));color:var(--vot-helper-theme);fill:var(--vot-helper-theme);cursor:default;z-index:2147483647;opacity:0;align-items:center;width:max-content;max-width:calc(100vw - 10px);height:max-content;font-size:14px;line-height:1.5;transition:opacity .5s;display:flex;position:absolute;inset:0;overflow:hidden;box-shadow:0 1px 3px #0000001f;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border-radius:4px!important;padding:4px 8px!important}.vot-tooltip[data-trigger=click]{-webkit-user-select:text;user-select:text}.vot-tooltip[data-mode=follow]{pointer-events:auto;-webkit-user-select:text;user-select:text;align-items:stretch}.vot-tooltip[data-mode=follow],.vot-tooltip[data-mode=follow] *{-webkit-user-select:text!important;user-select:text!important}.vot-tooltip.vot-tooltip-bordered{border:1px solid var(--vot-helper-border)}.vot-tooltip *{box-sizing:border-box!important;font-family:inherit!important}.vot-tooltip.vot-tooltip--subtitles-info{overflow:visible;box-shadow:none!important;background:0 0!important;border-radius:18px!important;padding:0!important}.vot-tooltip.vot-tooltip--subtitles-info .vot-subtitles-info{flex-direction:column;gap:8px;width:max-content;max-width:min(420px,100vw - 24px);display:flex;color:#ffffffeb!important;letter-spacing:0!important;background:#1f2024f5!important;border:1px solid #ffffff14!important;border-radius:16px!important;padding:14px 16px!important;font-size:13px!important;line-height:1.35!important;box-shadow:0 12px 30px #00000047,0 2px 6px #00000038!important}.vot-tooltip.vot-tooltip--subtitles-info .vot-subtitles-info-title{flex-wrap:wrap;align-items:center;gap:6px;min-width:0;display:flex;font-size:15px!important;font-weight:600!important;line-height:1.35!important}.vot-tooltip.vot-tooltip--subtitles-info .vot-subtitles-info-source{overflow-wrap:anywhere;color:#fffffff0!important}.vot-tooltip.vot-tooltip--subtitles-info .vot-subtitles-info-divider{color:#ffffff6b!important;font-weight:500!important}.vot-tooltip.vot-tooltip--subtitles-info .vot-subtitles-info-header{overflow-wrap:anywhere;color:rgb(var(--vot-primary-rgb,255, 83, 151))!important}.vot-tooltip.vot-tooltip--subtitles-info .vot-subtitles-info-context{overflow-wrap:anywhere;max-width:100%;color:#ffffffad!important;font-size:13px!important;font-weight:400!important;line-height:1.45!important}.vot-menu{--vot-helper-surface-rgb:var(--vot-surface-rgb,255, 255, 255);--vot-helper-surface:rgb(var(--vot-helper-surface-rgb));--vot-helper-onsurface-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-onsurface:rgba(var(--vot-helper-onsurface-rgb), .87);--vot-settings-control-width:clamp(120px, 45%, 200px);--vot-menu-default-top:calc(5rem + 48px);--vot-menu-top-offset:max(64px, calc(env(safe-area-inset-top,0px) + 56px));--vot-menu-side-offset:max(76px, calc(env(safe-area-inset-left,0px) + 64px));--vot-menu-side-offset-right:max(76px, calc(env(safe-area-inset-right,0px) + 64px));--vot-menu-side-top-offset:max(clamp(56px, 12.5vh, 136px), calc(env(safe-area-inset-top,0px) + 8px));left:50%;right:auto;top:var(--vot-menu-default-top);position:absolute;overflow:hidden}@media (pointer:coarse){.vot-menu{--vot-menu-default-top:calc(3rem + 48px);--vot-menu-top-offset:max(56px, calc(env(safe-area-inset-top,0px) + 48px));--vot-menu-side-offset:max(64px, calc(env(safe-area-inset-left,0px) + 54px));--vot-menu-side-offset-right:max(64px, calc(env(safe-area-inset-right,0px) + 54px));--vot-menu-side-top-offset:max(clamp(50px, 12.5vh, 120px), calc(env(safe-area-inset-top,0px) + 8px))}}.vot-menu{-webkit-user-select:none;user-select:none;background-color:var(--vot-helper-surface);color:var(--vot-helper-onsurface);cursor:default;z-index:2147483646;visibility:visible;opacity:1;transform-origin:top;width:fit-content;min-width:320px;max-width:min(90vw,560px);transition:opacity var(--vot-duration-medium) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard);font-size:16px;line-height:1.5;transform:translate(-50%)scale(1);border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-m)!important;box-shadow:var(--vot-shadow-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important}.vot-menu *{box-sizing:border-box!important}.vot-menu[hidden]{pointer-events:none;visibility:hidden;opacity:0;transform:translate(-50%,-4px)scale(.98);display:block!important}.vot-menu-content-wrapper{min-width:320px;min-height:100px;max-height:calc(var(--vot-container-height,75vh) - (5rem + 32px + 16px) * 2);flex-direction:column;display:flex;overflow:auto}.vot-menu-header-container{flex-shrink:0;align-items:center;min-height:31px;display:flex;padding-inline-end:var(--vot-space-2)!important}.vot-menu-header-container:empty{padding:0 0 16px!important}.vot-menu-header-container>.vot-icon-button{margin-inline-end:var(--vot-space-1)!important;margin-top:var(--vot-space-1)!important}.vot-menu-title-container{font-size:inherit;text-align:start;outline:0;flex:1;display:flex;font-weight:inherit!important;margin:0!important}.vot-menu-title{flex:1;font-size:16px;line-height:1;padding:var(--vot-space-4)!important;font-weight:500!important}.vot-menu-body-container{box-sizing:border-box;gap:var(--vot-space-2);overscroll-behavior:contain;flex-direction:column;min-height:1.375rem;display:flex;overflow:auto;padding:0 var(--vot-space-4)!important;scrollbar-color:rgba(var(--vot-helper-onsurface-rgb), .1) var(--vot-helper-surface)!important}.vot-menu-body-container::-webkit-scrollbar{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-menu-body-container::-webkit-scrollbar-track{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-menu-body-container::-webkit-scrollbar-thumb{border-radius:1ex;background:rgba(var(--vot-helper-onsurface-rgb), .1)!important;border:5px solid var(--vot-helper-surface)!important}.vot-menu-body-container::-webkit-scrollbar-thumb:hover{border-width:3px!important}.vot-menu-body-container::-webkit-scrollbar-corner{background:var(--vot-helper-surface)!important}.vot-menu-footer-container{flex-shrink:0;justify-content:flex-end;display:flex;padding:var(--vot-space-4)!important}.vot-menu-footer-container:empty{padding:var(--vot-space-4) 0 0 0!important}.vot-menu .vot-select--labeled>.vot-select-outer{margin-left:auto}.vot-menu[data-position=left]{left:var(--vot-menu-side-offset);right:auto;top:var(--vot-menu-side-top-offset);transform-origin:0 0;transform:scale(1)}.vot-menu[data-position=right]{left:auto;right:var(--vot-menu-side-offset-right);top:var(--vot-menu-side-top-offset);transform-origin:100% 0;transform:scale(1)}.vot-menu[data-position=leftCenter]{left:var(--vot-menu-side-offset);transform-origin:0;top:50%;right:auto;transform:translateY(-50%)scale(1)}.vot-menu[data-position=rightCenter]{left:auto;right:var(--vot-menu-side-offset-right);transform-origin:100%;top:50%;transform:translateY(-50%)scale(1)}.vot-menu[data-position=left][hidden],.vot-menu[data-position=right][hidden]{transform:translateY(-4px)scale(.98)}.vot-menu[data-position=leftCenter][hidden],.vot-menu[data-position=rightCenter][hidden]{transform:translateY(calc(-50% - 4px))scale(.98)}.vot-voice-icon{display:block;overflow:visible}.vot-voice-icon .vot-eq-bar{transform-origin:50% 100%;transform-box:fill-box;transform:scaleY(1)}.vot-voice-icon--standard .vot-eq-bar{fill:rgba(var(--vot-onsurface-rgb,227, 227, 227), .4)}.vot-voice-icon--live .vot-eq-bar{fill:#e040a0}.vot-voice-popover{--vot-helper-surface-rgb:var(--vot-surface-rgb,32, 33, 36);--vot-helper-surface:rgb(var(--vot-helper-surface-rgb));--vot-helper-onsurface-rgb:var(--vot-onsurface-rgb,227, 227, 227);--vot-helper-onsurface:rgba(var(--vot-helper-onsurface-rgb), .87);--vot-helper-onsurface-secondary:rgba(var(--vot-helper-onsurface-rgb), .55);--vot-voice-active-standard-bg:rgba(var(--vot-primary-rgb,139, 180, 245), .1);--vot-voice-active-standard-fg:rgb(var(--vot-primary-rgb,139, 180, 245));--vot-voice-active-live-bg:#e040a01a;--vot-voice-active-live-fg:#e040a0;z-index:2147483647;background:var(--vot-helper-surface);min-width:230px;max-width:var(--vot-voice-popover-max-width,310px);max-height:var(--vot-voice-popover-max-height,calc(100vh - 16px));cursor:default;-webkit-user-select:none;user-select:none;overscroll-behavior:contain;transform-origin:0 0;width:max-content;display:block;position:absolute;top:0;left:0;overflow:hidden auto;border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-m)!important;box-shadow:var(--vot-shadow-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;text-align:left!important}.vot-voice-popover,.vot-voice-popover *{box-sizing:border-box!important}.vot-voice-popover{opacity:0;visibility:hidden;pointer-events:none;transition:none;transform:none}.vot-voice-popover[hidden]{display:none!important}.vot-voice-popover:not([hidden]),.vot-voice-popover.is-open,.vot-voice-popover[aria-hidden=false]{opacity:1;visibility:visible;pointer-events:auto;transform:none}.vot-voice-popover[data-placement=top]{transform-origin:bottom}.vot-voice-popover[data-placement=bottom]{transform-origin:top}.vot-voice-popover[data-placement=left]{transform-origin:100%}.vot-voice-popover[data-placement=right]{transform-origin:0}.vot-voice-popover.is-closing{opacity:0;visibility:visible;pointer-events:none;transform:none}.vot-voice-popover__item{cursor:pointer;min-height:62px;color:var(--vot-helper-onsurface);outline:none;align-items:center;gap:12px;transition:none;display:flex;position:relative;overflow:hidden;padding:14px 44px 14px 16px!important}.vot-voice-popover__item:before{content:\"\";opacity:0;pointer-events:none;background:linear-gradient(180deg, rgba(var(--vot-helper-onsurface-rgb), .045), rgba(var(--vot-helper-onsurface-rgb), .06));transition:none;position:absolute;inset:0}.vot-voice-popover__item:hover,.vot-voice-popover__item:focus-visible{box-shadow:inset 0 1px #ffffff08,inset 0 -1px #0000000a}.vot-voice-popover__item:hover:before,.vot-voice-popover__item:focus-visible:before{opacity:1}.vot-voice-popover__item:focus-visible{box-shadow:inset 0 0 0 1px rgba(var(--vot-primary-rgb,139, 180, 245), .18)}.vot-voice-popover__item:after{content:\"\";opacity:0;background-color:currentColor;width:18px;height:18px;transition:none;position:absolute;top:50%;right:14px;transform:translateY(-50%)scale(.88);-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E\") 50%/contain no-repeat;mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E\") 50%/contain no-repeat}.vot-voice-popover__item--active{font-weight:500!important}.vot-voice-popover__item--active:after{opacity:1;transform:translateY(-50%)scale(1)}.vot-voice-popover__item[data-voice=standard].vot-voice-popover__item--active{background-color:var(--vot-voice-active-standard-bg);color:var(--vot-voice-active-standard-fg)}.vot-voice-popover__item[data-voice=standard].vot-voice-popover__item--active .vot-voice-popover__item-title{color:inherit}.vot-voice-popover__item[data-voice=standard].vot-voice-popover__item--active .vot-voice-icon--standard .vot-eq-bar{fill:currentColor}.vot-voice-popover__item[data-voice=live].vot-voice-popover__item--active{background-color:var(--vot-voice-active-live-bg);color:var(--vot-voice-active-live-fg)}.vot-voice-popover__item[data-voice=live].vot-voice-popover__item--active .vot-voice-popover__item-title{color:inherit}.vot-voice-popover__item-icon{flex-shrink:0;justify-content:center;align-items:flex-end;width:28px;height:28px;display:flex}.vot-voice-popover__item-icon svg{width:20px;height:20px;display:block}.vot-voice-popover__item-text{flex-direction:column;gap:2px;min-width:0;display:flex}.vot-voice-popover__item-title{color:inherit;white-space:nowrap;font-size:15px;line-height:1.3;transition:none;font-weight:400!important}.vot-voice-popover__item-subtitle{color:var(--vot-helper-onsurface-secondary);white-space:normal;font-size:12px;line-height:1.4}.vot-voice-popover__divider{background:var(--vot-border-color);height:1px;margin:0!important}@media (prefers-reduced-motion:reduce){.vot-voice-icon .vot-eq-bar{animation:none!important;transform:scaleY(1)!important}.vot-voice-popover,.vot-voice-popover *,.vot-voice-popover:before,.vot-voice-popover:after{transition:none!important;animation:none!important}.vot-voice-popover,.vot-voice-popover.is-closing,.vot-voice-popover:not([hidden]),.vot-voice-popover.is-open,.vot-voice-popover[aria-hidden=false]{transform:none!important}}.vot-dialog{--vot-helper-surface-rgb:var(--vot-surface-rgb,255, 255, 255);--vot-helper-surface:rgb(var(--vot-helper-surface-rgb));--vot-helper-onsurface-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-onsurface:rgba(var(--vot-helper-onsurface-rgb), .87);--vot-dialog-viewport-margin:16px;--vot-dialog-max-height:75vh;max-width:initial;max-height:initial;width:min(var(--vot-dialog-width,512px), 100%);border:1px solid var(--vot-border-color);border-radius:var(--vot-radius-l);background-color:var(--vot-helper-surface);height:fit-content;color:var(--vot-helper-onsurface);box-shadow:var(--vot-shadow-2);-webkit-user-select:none;user-select:none;visibility:visible;opacity:1;transform-origin:50%;transition:opacity var(--vot-duration-medium) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard);font-size:16px;line-height:1.5;display:block;position:fixed;inset-block:0;inset-inline:0;overflow:auto hidden;transform:scale(1);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;margin:auto!important;padding:0!important}[hidden]>.vot-dialog{pointer-events:none;opacity:0;transition:opacity var(--vot-duration-fast) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard);transform:translateY(-4px)scale(.98)}.vot-dialog[data-vertical-align=top]{inset-block-start:var(--vot-dialog-viewport-margin);inset-block-end:auto;margin:0 auto!important}.vot-dialog-container{visibility:visible;z-index:2147483647;position:absolute}.vot-dialog-container[hidden]{pointer-events:none;visibility:hidden;display:block!important}.vot-dialog-container *{box-sizing:border-box!important}.vot-dialog-backdrop{opacity:1;background-color:#0009;transition:opacity .3s;position:fixed;inset:0}[hidden]>.vot-dialog-backdrop{pointer-events:none;opacity:0}.vot-dialog-content-wrapper{max-height:var(--vot-dialog-max-height,75vh);flex-direction:column;display:flex;overflow:auto}.vot-dialog-header-container{flex-shrink:0;align-items:flex-start;min-height:31px;display:flex}.vot-dialog-header-container:empty{padding:0 0 20px}.vot-dialog-header-container>.vot-icon-button{margin-inline-end:var(--vot-space-1)!important;margin-top:var(--vot-space-1)!important}.vot-dialog-title-container{font-size:inherit;outline:0;flex:1;display:flex;font-weight:inherit!important;margin:0!important}.vot-dialog-title{flex:1;font-size:115.385%;line-height:1;padding:var(--vot-space-5) var(--vot-space-5) var(--vot-space-4)!important;font-weight:700!important}.vot-dialog-body-container{box-sizing:border-box;gap:var(--vot-space-4);overscroll-behavior:contain;flex-direction:column;min-height:1.375rem;display:flex;overflow:auto;padding:0 var(--vot-space-5)!important;scrollbar-color:rgba(var(--vot-helper-onsurface-rgb), .1) var(--vot-helper-surface)!important}.vot-dialog-body-container::-webkit-scrollbar{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-dialog-body-container::-webkit-scrollbar-track{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-dialog-body-container::-webkit-scrollbar-thumb{border-radius:1ex;background:rgba(var(--vot-helper-onsurface-rgb), .1)!important;border:5px solid var(--vot-helper-surface)!important}.vot-dialog-body-container::-webkit-scrollbar-thumb:hover{border-width:3px!important}.vot-dialog-body-container::-webkit-scrollbar-corner{background:var(--vot-helper-surface)!important}.vot-dialog-footer-container{justify-content:flex-end;gap:var(--vot-space-2);flex-wrap:wrap;flex-shrink:0;display:flex;padding:var(--vot-space-4)!important}.vot-dialog-footer-container:empty{padding:var(--vot-space-5) 0 0 0!important}@media (width<=480px){.vot-dialog-footer-container{flex-direction:column;align-items:stretch}.vot-dialog-footer-container>:is(.vot-button,.vot-outlined-button,.vot-text-button){white-space:normal;text-overflow:clip;text-align:center;justify-content:center;align-items:center;width:100%;height:auto;min-height:36px;padding:8px 16px;line-height:1.2;display:flex;overflow:visible}}.vot-inline-loader{aspect-ratio:5;--vot-loader-bg:no-repeat radial-gradient(farthest-side, rgba(var(--vot-onsurface-rgb,0, 0, 0), .38) 94%, transparent);background:var(--vot-loader-bg), var(--vot-loader-bg), var(--vot-loader-bg), var(--vot-loader-bg);background-size:20% 100%;height:8px;animation:.75s infinite alternate dotsSlide,1.5s infinite alternate dotsFlip}.vot-loader-progress{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));fill:none;stroke:rgb(var(--vot-helper-theme));stroke-width:2px;stroke-linecap:round;transform-origin:50%;transform:rotate(-90deg)}@keyframes dotsSlide{0%,10%{background-position:0 0,0 0,0 0,0 0}33%{background-position:0 0,33.3333% 0,33.3333% 0,33.3333% 0}66%{background-position:0 0,33.3333% 0,66.6667% 0,66.6667% 0}90%,to{background-position:0 0,33.3333% 0,66.6667% 0,100% 0}}@keyframes dotsFlip{0%,49.99%{transform:scale(1)}50%,to{transform:scale(-1)}}.vot-label{font-family:inherit;font-size:16px;line-height:1.5;display:block}.vot-label-text{display:inline}.vot-label-icon{vertical-align:text-bottom;cursor:help;justify-content:center;align-items:center;width:20px;height:20px;margin-left:4px;display:inline-flex}.vot-label-icon>svg{width:20px;height:20px;display:block}.vot-account{justify-content:space-between;align-items:center;gap:1rem;display:flex}.vot-account-container,.vot-account-wrapper,.vot-account-buttons{align-items:center;gap:1rem;display:flex}.vot-account-avatar{min-width:36px;max-width:36px;min-height:36px;max-height:36px;overflow:hidden}.vot-account-avatar-img{object-fit:cover;border-radius:50%;width:36px;height:36px}@property --vot-subtitles-opacity{syntax:\"\";inherits:true;initial-value:.8}@property --vot-subtitles-scale-compensation{syntax:\"\";inherits:true;initial-value:1}.vot-subtitles{--vot-subtitles-background:rgba(var(--vot-surface-rgb,46, 47, 52), var(--vot-subtitles-opacity,.8));--vot-subtitles-effective-max-width:var(--vot-subtitles-max-width,var(--vot-subtitles-smart-max-width,70vw));max-width:var(--vot-subtitles-effective-max-width);max-inline-size:var(--vot-subtitles-effective-max-width);background:var(--vot-subtitles-background,#2e2f34cc);width:max-content;inline-size:max-content;color:var(--vot-subtitles-color,#e3e3e3);pointer-events:all;touch-action:none;font-size:calc(var(--vot-subtitles-font-size,clamp(18px, var(--vot-subtitles-smart-font-preferred,2.2vw), 50px)) * var(--vot-subtitles-scale-compensation,1));-webkit-text-stroke:var(--vot-subtitles-text-stroke-width,clamp(1px, .08em, 2px)) var(--vot-subtitles-text-stroke-color,#000000eb);paint-order:stroke fill;text-shadow:var(--vot-subtitles-text-shadow,0 1px 2px #00000073, 0 2px 8px #00000040);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-synthesis:none;position:relative;--vot-subtitles-font-family:var(--vot-subtitles-font-family-custom,var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif))!important;font-family:var(--vot-subtitles-font-family)!important;font-style:normal!important;font-weight:var(--vot-subtitles-font-weight,500)!important;text-transform:none!important;letter-spacing:normal!important;border-radius:.5em!important;padding:.5em .75em!important;line-height:1.25!important}.vot-subtitles,.vot-subtitles *{-webkit-text-stroke:inherit;paint-order:inherit;font-family:var(--vot-subtitles-font-family)!important}.vot-subtitles{box-sizing:border-box;-webkit-user-select:none;user-select:none;contain:layout paint;isolation:isolate;text-align:center;text-wrap:balance;white-space:normal;overflow-wrap:anywhere;unicode-bidi:plaintext;margin:0 auto;display:block}.vot-subtitles-widget{--vot-subtitles-anchor-width:100vw;--vot-subtitles-anchor-height:100vh;--vot-subtitles-effective-max-width:var(--vot-subtitles-max-width,var(--vot-subtitles-smart-max-width,70vw));--vot-subtitles-smart-target-width:48ch;--vot-subtitles-smart-min-width-ratio:.62;--vot-subtitles-smart-max-width-ratio:.78;--vot-subtitles-smart-font-preferred:calc(var(--vot-subtitles-anchor-height) * .0333);--vot-subtitles-smart-max-width:clamp(calc(var(--vot-subtitles-anchor-width) * var(--vot-subtitles-smart-min-width-ratio)), var(--vot-subtitles-smart-target-width), calc(var(--vot-subtitles-anchor-width) * var(--vot-subtitles-smart-max-width-ratio)));box-sizing:border-box;z-index:2147483647;--vot-subtitles-fallback-bottom-inset:calc(env(safe-area-inset-bottom,0px) + clamp(56px, 10vh, 220px) + 10px);left:50%;top:calc(100% - var(--vot-subtitles-fallback-bottom-inset));width:max-content;inline-size:max-content;max-width:var(--vot-subtitles-effective-max-width);max-inline-size:var(--vot-subtitles-effective-max-width);pointer-events:none;will-change:left, top, transform;max-height:100%;display:block;position:absolute;transform:translate(-50%,-100%)}.vot-subtitles-info{color:#f5f7fa;-webkit-backdrop-filter:blur(18px);text-align:start;background:#1f2023f5;border:1px solid #ffffff14;flex-direction:column;gap:10px;min-width:min(360px,100vw - 32px);max-width:min(720px,100vw - 32px);display:flex;box-shadow:0 18px 48px #00000057,0 4px 16px #00000038;border-radius:18px!important;padding:18px 22px 20px!important}.vot-subtitles-info-service{display:none!important}.vot-subtitles-info-title{letter-spacing:-.01em;align-items:baseline;gap:8px;min-width:0;max-width:100%;display:flex;font-size:clamp(18px,2.4vw,26px)!important;line-height:1.18!important}.vot-subtitles-info-source,.vot-subtitles-info-divider,.vot-subtitles-info-header,.vot-subtitles-info-context{overflow-wrap:anywhere;white-space:normal!important}.vot-subtitles-info-source{color:#f5f7faad;flex:0 auto;font-weight:650!important}.vot-subtitles-info-divider{color:#f5f7fa61;flex:none;font-weight:500!important}.vot-subtitles-info-header{color:#fff;flex:auto;min-width:0;font-weight:750!important}.vot-subtitles-info-context{color:#dee4ecb8;max-width:100%;font-size:clamp(14px,1.45vw,17px)!important;line-height:1.42!important}.vot-subtitles span[data-vot-highlight-index].passed{color:var(--vot-subtitles-passed-color,#2196f3)}.vot-subtitles span[data-vot-token=\"1\"]{cursor:pointer;white-space:normal;overflow-wrap:inherit;word-break:normal;position:relative;font-size:inherit!important;font-family:inherit!important;font-style:inherit!important;font-weight:inherit!important;line-height:inherit!important;text-transform:inherit!important;text-decoration:none!important}.vot-subtitles span[data-vot-token=\"1\"]:before{content:\"\";z-index:-1;position:absolute;inset:2px -2px;border-radius:4px!important}.vot-subtitles span[data-vot-token=\"1\"]:hover:before{background:var(--vot-subtitles-hover-color,#ffffff8c)}.vot-subtitles span[data-vot-token=\"1\"].selected:before{background:var(--vot-subtitles-passed-color,#2196f3)}.vot-subtitles span[data-vot-style-italic=\"1\"]{font-style:italic!important}.vot-subtitles span[data-vot-style-bold=\"1\"]{font-weight:700!important}.vot-subtitles span[data-vot-style-underline=\"1\"]{text-decoration:underline!important}.vot-subtitles span[data-vot-style-color=\"1\"]{color:var(--vot-subtitles-inline-color)!important}.vot-subtitles-layer{pointer-events:none;z-index:2147483647;contain:layout paint;width:100vw!important;height:100vh!important;position:fixed!important;inset:0!important}.vot-subtitles-guides{pointer-events:none;z-index:2147483646;position:absolute;inset:0}.vot-subtitles-guide{background:rgba(var(--vot-primary-rgb,33, 150, 243), .7);box-shadow:0 0 0 1px rgba(var(--vot-primary-rgb,33, 150, 243), .12);opacity:0;transition:opacity .12s linear;position:absolute}.vot-subtitles-guide[data-visible=true]{opacity:1}.vot-subtitles-guide--vertical{width:2px;transform:translate(-50%)}.vot-subtitles-guide--horizontal{height:2px;transform:translateY(-50%)}@media (aspect-ratio<=1){.vot-subtitles-widget{--vot-subtitles-smart-target-width:28ch;--vot-subtitles-smart-min-width-ratio:.8;--vot-subtitles-smart-max-width-ratio:.92;--vot-subtitles-smart-font-preferred:calc(var(--vot-subtitles-anchor-height) * .0296)}}@media (aspect-ratio>=1) and (aspect-ratio<=7/5){.vot-subtitles-widget{--vot-subtitles-smart-target-width:32ch;--vot-subtitles-smart-min-width-ratio:.55;--vot-subtitles-smart-max-width-ratio:.9;--vot-subtitles-smart-font-preferred:calc(var(--vot-subtitles-anchor-height) * .0333)}}@media (width<=900px) and (pointer:coarse){.vot-subtitles-widget{--vot-subtitles-fallback-bottom-inset:env(safe-area-inset-bottom,0px)}}@media (prefers-contrast:more){.vot-subtitles{--vot-subtitles-background:rgba(var(--vot-surface-rgb,46, 47, 52), .92);--vot-subtitles-text-stroke-width:max(2px, .1em);--vot-subtitles-text-shadow:0 2px 10px #0000008c}}:is(:fullscreen .vot-subtitles-widget,:fullscreen .vot-subtitles-widget){--vot-subtitles-smart-max-width-ratio:.8}:is(:fullscreen .vot-subtitles,:fullscreen .vot-subtitles){font-size:calc(var(--vot-subtitles-font-size,clamp(18px, var(--vot-subtitles-smart-font-preferred,2vw), 50px)) * var(--vot-subtitles-fullscreen-scale,1) * .95 * var(--vot-subtitles-scale-compensation,1))}#vot-subtitles-info.vot-subtitles-info *{-webkit-user-select:text!important;user-select:text!important}:root{--vot-font-family:\"Roboto\", \"Segoe UI\", system-ui, sans-serif;--vot-primary-rgb:139, 180, 245;--vot-onprimary-rgb:32, 33, 36;--vot-surface-rgb:32, 33, 36;--vot-onsurface-rgb:227, 227, 227;--vot-subtitles-color:rgb(var(--vot-onsurface-rgb,227, 227, 227));--vot-subtitles-passed-color:rgb(var(--vot-primary-rgb,33, 150, 243));--vot-space-1:4px;--vot-space-2:8px;--vot-space-3:12px;--vot-space-4:16px;--vot-space-5:20px;--vot-space-6:24px;--vot-radius-xs:6px;--vot-radius-s:10px;--vot-radius-m:14px;--vot-radius-l:18px;--vot-border-color:rgba(var(--vot-onsurface-rgb,227, 227, 227), .14);--vot-border-color-hover:rgba(var(--vot-onsurface-rgb,227, 227, 227), .22);--vot-shadow-1:0 1px 2px #0000002e, 0 8px 24px #00000024;--vot-shadow-2:0 2px 4px #00000038, 0 12px 32px #00000038;--vot-duration-fast:.12s;--vot-duration-medium:.2s;--vot-duration-slow:.32s;--vot-easing-standard:cubic-bezier(.4, 0, .2, 1);--vot-focus-ring-color:rgba(var(--vot-primary-rgb,139, 180, 245), .9);--vot-focus-ring:0 0 0 2px var(--vot-focus-ring-color);--vot-focus-ring-offset:0 0 0 4px rgba(var(--vot-surface-rgb,32, 33, 36), .9)}vot-block,vot-block *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}vot-block[hidden]:not(.vot-menu):not(.vot-dialog-container),vot-block [hidden]:not(.vot-menu):not(.vot-dialog-container){display:none!important}vot-block{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;display:block;--vot-font-family:\"Roboto\", \"Segoe UI\", system-ui, sans-serif!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;visibility:visible!important;font-weight:400!important}vot-block *{font-weight:inherit!important}.vot-portal-local,.vot-subtitles-widget{isolation:isolate}vot-block:focus,vot-block :focus{box-shadow:none!important;outline:none!important}html.vot-keyboard-nav vot-block:focus-visible,html.vot-keyboard-nav vot-block :focus-visible{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}@supports not selector(:focus-visible){html.vot-keyboard-nav vot-block:focus,html.vot-keyboard-nav vot-block :focus{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}}@media (prefers-reduced-motion:reduce){.vot-portal-local *,.vot-portal *,.vot-subtitles-widget *{scroll-behavior:auto!important;transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.vot-portal{display:contents}.vot-portal-local{z-index:2147483647;position:fixed;top:0;left:0}.vot-segmented-button,.vot-menu{pointer-events:auto}"); var sharedShadowStyleSheet = null; var sharedShadowStyleSheetReady = false; function scopeCssForShadowRoots(cssText) { return cssText.replaceAll(":root", ":host").replaceAll("html.vot-keyboard-nav", ":host-context(.vot-keyboard-nav)").replace(/:fullscreen(?=\s|,)/g, ":host-context(:fullscreen)").replace(/:-webkit-full-screen(?=\s|,)/g, ":host-context(:-webkit-full-screen)"); } function applyClasses(element, classes) { if (classes?.length) element.classList.add(...classes); } function applyInlineStyles(element, styles) { if (!styles) return; for (const [name, value] of Object.entries(styles)) { if (typeof value !== "string") continue; element.style.setProperty(name, value); } } function createMountElement({ tag, classes = [], styles }) { const element = document.createElement(tag); applyClasses(element, classes); applyInlineStyles(element, styles); return element; } function getSharedShadowStyleSheet() { if (sharedShadowStyleSheetReady) return sharedShadowStyleSheet; sharedShadowStyleSheetReady = true; if (!(typeof CSSStyleSheet !== "undefined" && typeof CSSStyleSheet.prototype.replaceSync === "function")) return null; const sheet = new CSSStyleSheet(); sheet.replaceSync(shadowScopedCssText); sharedShadowStyleSheet = sheet; return sharedShadowStyleSheet; } function adoptScopedStyles(shadowRoot) { const sharedSheet = getSharedShadowStyleSheet(); if (sharedSheet) { if (!shadowRoot.adoptedStyleSheets.includes(sharedSheet)) shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sharedSheet]; return; } const style = document.createElement("style"); style.textContent = shadowScopedCssText; shadowRoot.append(style); } function createShadowMount({ parent, hostTag = "vot-shadow-host", rootTag = "vot-block", hostClasses = [], rootClasses = [], hostStyles, rootStyles, delegatesFocus = false }) { const host = createMountElement({ tag: hostTag, classes: hostClasses, styles: hostStyles }); const shadowRoot = Element.prototype.attachShadow.call(host, { mode: "open", delegatesFocus }); adoptScopedStyles(shadowRoot); const root = createMountElement({ tag: rootTag, classes: rootClasses, styles: rootStyles }); shadowRoot.append(root); parent.append(host); return { host, root, shadowRoot }; } function reparentShadowMount(mount, parent) { if (!mount) return; if (mount.host.parentNode !== parent) parent.append(mount.host); } function destroyShadowMount(mount) { mount?.host.remove(); } //#endregion //#region src/subtitles/layoutController.ts function isTimeInLine(time, line) { return time >= line.startMs && time < line.startMs + line.durationMs; } //#endregion //#region src/subtitles/activeCues.ts var createFallbackTokens = (line) => { if (line.tokens.length) return line.tokens; const text = line.text.trim(); if (!text) return []; return [{ text, startMs: line.startMs, durationMs: line.durationMs, isWordLike: Boolean(text) }]; }; var toRenderableTextKey = (line) => { return (line.text || createFallbackTokens(line).map((token) => token.text).join("")).replaceAll(/\s+/gu, " ").trim(); }; var linesOverlapInTime = (left, right) => { const leftEnd = left.startMs + Math.max(0, left.durationMs); const rightEnd = right.startMs + Math.max(0, right.durationMs); return left.startMs < rightEnd && right.startMs < leftEnd; }; var dedupeActiveLines = (lines) => { const deduped = []; for (const entry of lines) { const textKey = toRenderableTextKey(entry.line); if (!textKey) continue; if (!deduped.some((existing) => { return textKey === toRenderableTextKey(existing.line) && existing.line.speakerId === entry.line.speakerId && linesOverlapInTime(existing.line, entry.line); })) deduped.push(entry); } return deduped; }; var findLastCueIndexStartingAtOrBefore = (time, subtitlesList) => { let low = 0; let high = subtitlesList.length - 1; let candidate = -1; while (low <= high) { const mid = low + high >> 1; if (subtitlesList[mid].startMs <= time) { candidate = mid; low = mid + 1; continue; } high = mid - 1; } return candidate; }; var findActiveSubtitleLineIndices = (time, subtitlesList, maxCueDurationMs = Number.POSITIVE_INFINITY) => { const lastCueIndex = findLastCueIndexStartingAtOrBefore(time, subtitlesList); if (lastCueIndex < 0) return []; const minStartMs = Number.isFinite(maxCueDurationMs) ? Math.max(0, time - Math.max(0, maxCueDurationMs)) : Number.NEGATIVE_INFINITY; const activeLineIndices = []; for (let index = lastCueIndex; index >= 0; index -= 1) { const line = subtitlesList[index]; if (line.startMs < minStartMs) break; if (isTimeInLine(time, line)) activeLineIndices.push(index); } activeLineIndices.reverse(); return activeLineIndices; }; var buildActiveSubtitleRenderLine = (time, subtitlesList, maxCueDurationMs = Number.POSITIVE_INFINITY) => { const activeLineIndices = findActiveSubtitleLineIndices(time, subtitlesList, maxCueDurationMs); if (!activeLineIndices.length) return null; const activeEntries = dedupeActiveLines(activeLineIndices.map((index) => ({ index, line: subtitlesList[index] }))); if (!activeEntries.length) return null; if (activeEntries.length === 1) { const [entry] = activeEntries; return { line: entry.line, lineKey: `${entry.index}` }; } const tokens = []; const textParts = []; const rawTextParts = []; const lineKeyParts = []; let earliestStartMs = Number.POSITIVE_INFINITY; let latestEndMs = 0; for (const entry of activeEntries) { const lineTokens = createFallbackTokens(entry.line); if (!lineTokens.length) continue; if (tokens.length > 0) { const breakStartMs = Math.max(earliestStartMs, entry.line.startMs); tokens.push({ text: "\n", startMs: breakStartMs, durationMs: 0, isWordLike: false }); } tokens.push(...lineTokens); textParts.push(entry.line.text || lineTokens.map((token) => token.text).join("")); rawTextParts.push(entry.line.metadata?.rawText ?? entry.line.text); lineKeyParts.push(`${entry.index}`); earliestStartMs = Math.min(earliestStartMs, entry.line.startMs); latestEndMs = Math.max(latestEndMs, entry.line.startMs + Math.max(0, entry.line.durationMs)); } if (!tokens.length || !lineKeyParts.length) return null; return { line: { text: textParts.join("\n"), startMs: earliestStartMs, durationMs: Math.max(0, latestEndMs - earliestStartMs), speakerId: activeEntries[0]?.line.speakerId ?? "0", tokens, metadata: rawTextParts.length ? { rawText: rawTextParts.join("\n") } : void 0 }, lineKey: lineKeyParts.join(",") }; }; //#endregion //#region src/types/subtitles.ts var subtitleFormats = [ "srt", "vtt", "ass", "json" ]; var subtitleFontFamilies = [ "default-sans", "arial", "helvetica", "roboto", "verdana", "open-sans", "poppins", "lato", "montserrat", "barlow" ]; var subtitleFontFamilyCss = { "default-sans": `"Roboto", "Segoe UI", system-ui, sans-serif`, arial: `Arial, "Helvetica Neue", Helvetica, sans-serif`, helvetica: `"Helvetica Neue", Helvetica, Arial, sans-serif`, roboto: `"Roboto", "Segoe UI", system-ui, sans-serif`, verdana: `Verdana, Geneva, sans-serif`, "open-sans": `"Open Sans", "Segoe UI", system-ui, sans-serif`, poppins: `"Poppins", "Segoe UI", system-ui, sans-serif`, lato: `"Lato", "Segoe UI", system-ui, sans-serif`, montserrat: `"Montserrat", "Segoe UI", system-ui, sans-serif`, barlow: `"Barlow", "Segoe UI", system-ui, sans-serif` }; //#endregion //#region src/subtitles/types.ts var subtitleFormatsSet = new Set(subtitleFormats); function isSubtitleFormat(value) { return typeof value === "string" && subtitleFormatsSet.has(value); } function isRecord(value) { return value !== null && typeof value === "object"; } function parseSubtitleDescriptor(value) { if (!isRecord(value)) return null; const format = value.format; if (typeof value.source !== "string" || typeof value.language !== "string" || typeof value.url !== "string" || !isSubtitleFormat(format)) return null; return { source: value.source, format, language: value.language, url: value.url, translatedFromLanguage: typeof value.translatedFromLanguage === "string" ? value.translatedFromLanguage : void 0, isAutoGenerated: typeof value.isAutoGenerated === "boolean" ? value.isAutoGenerated : void 0 }; } function isBuiltInSubtitleFontFamily(fontFamily) { return subtitleFontFamilies.includes(fontFamily); } //#endregion //#region src/subtitles/fonts.ts var GOOGLE_SUBTITLE_FONT_PREFIX = "google:"; var GOOGLE_FONTS_CSS_API_URL = "https://fonts.googleapis.com/css2"; var GOOGLE_FONTS_METADATA_URL = "https://fonts.google.com/metadata/fonts"; var subtitleGoogleFontFamilyNames = { roboto: "Roboto", "open-sans": "Open Sans", poppins: "Poppins", lato: "Lato", montserrat: "Montserrat", barlow: "Barlow" }; var loadedSubtitleGoogleFonts = /* @__PURE__ */ new Set(); var pendingSubtitleGoogleFonts = /* @__PURE__ */ new Map(); var googleFontsCatalogPromise = null; function toGoogleSubtitleFontFamily(familyName) { return `${GOOGLE_SUBTITLE_FONT_PREFIX}${familyName}`; } function getGoogleSubtitleFontFamilyName(fontFamily) { if (fontFamily.startsWith(GOOGLE_SUBTITLE_FONT_PREFIX)) { const familyName = fontFamily.slice(7).trim(); return familyName.length > 0 ? familyName : null; } return subtitleGoogleFontFamilyNames[fontFamily] ?? null; } function getSubtitleFontFamilyCssValue(fontFamily) { if (isBuiltInSubtitleFontFamily(fontFamily)) return subtitleFontFamilyCss[fontFamily]; const googleFontFamilyName = getGoogleSubtitleFontFamilyName(fontFamily); if (googleFontFamilyName) return `"${googleFontFamilyName}", "Segoe UI", system-ui, sans-serif`; return subtitleFontFamilyCss["default-sans"]; } function buildGoogleFontsCssUrl(fontFamily) { const googleFontFamily = getGoogleSubtitleFontFamilyName(fontFamily); if (!googleFontFamily) return null; return `${GOOGLE_FONTS_CSS_API_URL}?family=${googleFontFamily.trim().replaceAll(/\s+/g, "+")}&display=swap`; } function injectFontStylesheet(fontFamily, cssText) { const styleId = `vot-google-font-${fontFamily}`; if (document.getElementById(styleId)) return; const gmAddStyle = globalThis.GM_addStyle; const styleElement = typeof gmAddStyle === "function" ? gmAddStyle(cssText) : document.createElement("style"); if (!(styleElement instanceof HTMLElement)) return; styleElement.id = styleId; if (!styleElement.textContent) styleElement.textContent = cssText; if (!styleElement.parentElement) (document.head || document.documentElement).appendChild(styleElement); } async function ensureGoogleSubtitleFontLoaded(fontFamily, options = {}) { if (loadedSubtitleGoogleFonts.has(fontFamily)) return; const existingLoad = pendingSubtitleGoogleFonts.get(fontFamily); if (existingLoad !== void 0) { await existingLoad; return; } const cssUrl = buildGoogleFontsCssUrl(fontFamily); if (!cssUrl) { loadedSubtitleGoogleFonts.add(fontFamily); return; } const googleFontFamily = getGoogleSubtitleFontFamilyName(fontFamily); const loadPromise = (async () => { try { const response = await GM_fetch(cssUrl, { timeout: 1e4, forceGmXhr: options.forceGmXhr ?? true, headers: { Accept: "text/css,*/*;q=0.1" } }); if (!response.ok) throw new Error(`Google Fonts CSS request failed with ${response.status}`); const cssText = await response.text(); if (!cssText.trim()) throw new Error("Google Fonts CSS response is empty"); injectFontStylesheet(fontFamily, cssText); loadedSubtitleGoogleFonts.add(fontFamily); if (document.fonts && googleFontFamily) await document.fonts.load(`500 20px "${googleFontFamily}"`); options.onLoaded?.(); } catch (error) { debug.log("Failed to load Google Font for subtitles", { fontFamily, error }); } finally { pendingSubtitleGoogleFonts.delete(fontFamily); } })(); pendingSubtitleGoogleFonts.set(fontFamily, loadPromise); await loadPromise; } async function loadGoogleFontsCatalog() { if (googleFontsCatalogPromise !== null) return await googleFontsCatalogPromise; googleFontsCatalogPromise = (async () => { const response = await GM_fetch(GOOGLE_FONTS_METADATA_URL, { timeout: 15e3, forceGmXhr: isSupportGMXhr }); if (!response.ok) throw new Error(`Google Fonts metadata request failed with ${response.status}`); const sanitizedText = (await response.text()).replace(/^\)\]\}'\n?/, ""); const fontFamilies = JSON.parse(sanitizedText).familyMetadataList?.map((entry) => entry.family?.trim() ?? "").filter((familyName) => familyName.length > 0); return Array.from(new Set(fontFamilies)).sort((left, right) => left.localeCompare(right)); })().catch((error) => { googleFontsCatalogPromise = null; debug.log("Failed to load Google Fonts catalog", error); return []; }); return await googleFontsCatalogPromise; } //#endregion //#region src/subtitles/fullscreenLayerController.ts var FullscreenLayerController = class { container; constructor({ container }) { this.container = container; } updateContainer(container) { this.container = container; } getWidgetParentElement() { return this.container; } getLayoutRootElement() { return this.container instanceof ShadowRoot ? this.container.host : this.container; } syncWidgetContainer(widgetContainer) { const containerEl = this.container instanceof ShadowRoot ? this.container.host : this.container; if (getComputedStyle(containerEl).position === "static") containerEl.style.position = "relative"; if (widgetContainer && widgetContainer.parentNode !== this.container) this.container.appendChild(widgetContainer); } release() {} }; //#endregion //#region src/subtitles/inlineStyle.ts var SAFE_CSS_COLOR_NAME_RE = /^[a-z]+$/iu; var SAFE_HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/iu; var SAFE_CSS_FUNCTION_COLOR_RE = /^(?:rgba?|hsla?)\([\d.,%\s/+_-]+\)$/iu; var SAFE_CLASS_NAME_RE = /^[a-z0-9_-]+$/iu; var normalizeClassNames = (classes) => { if (!classes?.length) return void 0; const normalized = Array.from(new Set(classes.map((value) => value.trim()).filter((value) => value && SAFE_CLASS_NAME_RE.test(value)))).sort((left, right) => left.localeCompare(right)); return normalized.length ? normalized : void 0; }; var normalizeCssColorValue = (value) => { const normalized = value.trim(); if (!normalized) return void 0; if (SAFE_HEX_COLOR_RE.test(normalized)) return normalized.toLowerCase(); if (SAFE_CSS_COLOR_NAME_RE.test(normalized)) return normalized.toLowerCase(); if (SAFE_CSS_FUNCTION_COLOR_RE.test(normalized)) return normalized; }; var normalizeSubtitleInlineStyle = (style) => { if (!style) return void 0; const normalized = {}; if (style.italic) normalized.italic = true; if (style.bold) normalized.bold = true; if (style.underline) normalized.underline = true; const normalizedColor = typeof style.color === "string" ? normalizeCssColorValue(style.color) : void 0; if (normalizedColor) normalized.color = normalizedColor; const normalizedClasses = normalizeClassNames(style.classes); if (normalizedClasses) normalized.classes = normalizedClasses; return Object.keys(normalized).length ? normalized : void 0; }; var sanitizeSubtitleInlineStyle = (value) => { if (!value || typeof value !== "object") return void 0; const raw = value; return normalizeSubtitleInlineStyle({ italic: raw.italic === true, bold: raw.bold === true, underline: raw.underline === true, color: typeof raw.color === "string" ? raw.color : void 0, classes: Array.isArray(raw.classes) ? raw.classes.filter((entry) => typeof entry === "string") : void 0 }); }; var subtitleInlineStylesEqual = (left, right) => { const leftNormalized = normalizeSubtitleInlineStyle(left); const rightNormalized = normalizeSubtitleInlineStyle(right); const leftClasses = leftNormalized?.classes ?? []; const rightClasses = rightNormalized?.classes ?? []; return Boolean(leftNormalized?.italic) === Boolean(rightNormalized?.italic) && Boolean(leftNormalized?.bold) === Boolean(rightNormalized?.bold) && Boolean(leftNormalized?.underline) === Boolean(rightNormalized?.underline) && (leftNormalized?.color ?? "") === (rightNormalized?.color ?? "") && leftClasses.length === rightClasses.length && leftClasses.every((value, index) => value === rightClasses[index]); }; var buildSubtitleInlineStyleCssText = (style) => { const normalized = normalizeSubtitleInlineStyle(style); if (!normalized?.color) return ""; return `--vot-subtitles-inline-color:${normalized.color};`; }; //#endregion //#region src/subtitles/positionController.ts var clampToRange = clampNumber; function hasDragThresholdBeenExceeded(startClientX, startClientY, nextClientX, nextClientY, thresholdPx) { const dx = nextClientX - startClientX; const dy = nextClientY - startClientY; return dx * dx + dy * dy >= thresholdPx * thresholdPx; } function getVerticalAnchorBounds({ elementHeight, boxHeight, bottomInset }) { const minAnchorY = Math.max(0, elementHeight || 0); const baselineAnchorY = Math.max(minAnchorY, boxHeight - bottomInset); return { minAnchorY, baselineAnchorY, travelPx: Math.max(0, baselineAnchorY - minAnchorY) }; } function captureCustomVerticalAnchorState({ anchorY, elementHeight, boxHeight, bottomInset }) { const { minAnchorY, baselineAnchorY, travelPx } = getVerticalAnchorBounds({ elementHeight, boxHeight, bottomInset }); return { offsetFromBaselinePx: clampToRange(anchorY, minAnchorY, baselineAnchorY) - baselineAnchorY, travelPx }; } function resolveCustomVerticalAnchor({ state, elementHeight, boxHeight, bottomInset }) { const { minAnchorY, baselineAnchorY, travelPx } = getVerticalAnchorBounds({ elementHeight, boxHeight, bottomInset }); if (!state || travelPx <= 0) return baselineAnchorY; const storedTravelPx = Math.max(0, state.travelPx || 0); const storedLiftPx = Math.max(0, -(state.offsetFromBaselinePx || 0)); if (storedTravelPx <= 0 || storedLiftPx <= 0) return baselineAnchorY; const ratioLiftPx = storedLiftPx / storedTravelPx * travelPx; return clampToRange(baselineAnchorY - (travelPx >= storedTravelPx ? Math.min(storedLiftPx, ratioLiftPx) : Math.min(travelPx, ratioLiftPx)), minAnchorY, baselineAnchorY); } function clampAnchorWithinBox({ anchorX, anchorY, elementWidth, elementHeight, boxWidth, boxHeight, bottomInset }) { let nextAnchorX = anchorX; let nextAnchorY = anchorY; const maxAnchorY = Math.max(0, boxHeight - bottomInset); const minAnchorY = elementHeight || 0; if (elementWidth) { let leftPx = nextAnchorX - elementWidth / 2; const maxLeftPx = boxWidth - elementWidth; if (maxLeftPx >= 0) leftPx = clampToRange(leftPx, 0, maxLeftPx); else leftPx = maxLeftPx / 2; nextAnchorX = leftPx + elementWidth / 2; } nextAnchorY = clampToRange(nextAnchorY, minAnchorY, maxAnchorY); return { anchorX: nextAnchorX, anchorY: nextAnchorY }; } function snapValueToNearestCandidate({ current, candidates, thresholdPx }) { let closestValue = current; let closestDistance = Number.POSITIVE_INFINITY; for (const candidate of candidates) { const distance = Math.abs(candidate - current); if (distance < closestDistance) { closestDistance = distance; closestValue = candidate; } } if (!Number.isFinite(closestDistance) || closestDistance > thresholdPx) return { snapped: false, value: current }; return { snapped: true, value: closestValue }; } //#endregion //#region src/subtitles/renderPlan.ts var PUNCTUATION_OR_SYMBOL_RE = /^[\p{P}\p{S}]$/u; var TEXT_TOKEN_SLICE_RE = /\s+|[\p{P}\p{S}]+|[^\s\p{P}\p{S}]+/gu; var isPunctuationOrSymbol = (char) => PUNCTUATION_OR_SYMBOL_RE.test(char); var getLeadingPunctuation = (value) => { let endIndex = 0; for (const char of value) { if (!isPunctuationOrSymbol(char)) break; endIndex += char.length; } return value.slice(0, endIndex); }; var getTrailingPunctuation = (value) => { const chars = Array.from(value); let length = 0; for (let index = chars.length - 1; index >= 0; index -= 1) { const char = chars[index]; if (!isPunctuationOrSymbol(char)) break; length += char.length; } return length > 0 ? value.slice(value.length - length) : ""; }; var isPunctuationOnly = (value) => value.length > 0 && Array.from(value).every(isPunctuationOrSymbol); var pushTextPart = (plan, text, style, options = {}) => { plan.push({ kind: "text", text, style, highlightIndex: options.highlightIndex }); if (options.withBreak) plan.push({ kind: "break" }); }; var skipWhitespaceTokens = (tokens, startIndex, renderEndTokenIndex) => { let index = startIndex; while (index <= renderEndTokenIndex && !tokens[index]?.isWordLike && !tokens[index]?.text.trim()) index += 1; return index; }; var hasFutureWordToken = (tokens, startIndex, renderEndTokenIndex) => { for (let index = startIndex; index <= renderEndTokenIndex; index += 1) { const tokenText = tokens[index]?.text ?? ""; if (!tokens[index]?.isWordLike || !tokenText.trim()) continue; const withoutLeadingWhitespace = tokenText.trimStart(); const leadingPunctuation = getLeadingPunctuation(withoutLeadingWhitespace); const withoutLeadingPunctuation = withoutLeadingWhitespace.slice(leadingPunctuation.length); const trailingPunctuation = getTrailingPunctuation(withoutLeadingPunctuation); if (trailingPunctuation ? withoutLeadingPunctuation.slice(0, -trailingPunctuation.length) : withoutLeadingPunctuation) return true; } return false; }; var consumeWordToken = (plan, tokens, startIndex, renderEndTokenIndex, breakAfterTokenIndexSet, highlightIndex) => { const token = tokens[startIndex]; const leadingWhitespace = /^\s+/u.exec(token.text)?.[0] ?? ""; const body = token.text.slice(leadingWhitespace.length); if (leadingWhitespace) pushTextPart(plan, leadingWhitespace, token.style); const leadingPunctuation = getLeadingPunctuation(body); const bodyWithoutLeadingPunctuation = body.slice(leadingPunctuation.length); const trailingPunctuation = getTrailingPunctuation(bodyWithoutLeadingPunctuation); const wordText = trailingPunctuation ? bodyWithoutLeadingPunctuation.slice(0, bodyWithoutLeadingPunctuation.length - trailingPunctuation.length) : bodyWithoutLeadingPunctuation; if (!wordText) { if (body) pushTextPart(plan, body, token.style); if (!breakAfterTokenIndexSet?.has(startIndex)) return { consumedWord: false, nextTokenIndex: startIndex + 1 }; plan.push({ kind: "break" }); return { consumedWord: false, nextTokenIndex: skipWhitespaceTokens(tokens, startIndex + 1, renderEndTokenIndex) }; } if (leadingPunctuation) pushTextPart(plan, leadingPunctuation, token.style, { highlightIndex }); plan.push({ kind: "word", text: wordText, style: token.style, highlightIndex }); if (trailingPunctuation) pushTextPart(plan, trailingPunctuation, token.style, { highlightIndex }); if (!breakAfterTokenIndexSet?.has(startIndex)) return { consumedWord: true, nextTokenIndex: startIndex + 1 }; plan.push({ kind: "break" }); return { consumedWord: true, nextTokenIndex: skipWhitespaceTokens(tokens, startIndex + 1, renderEndTokenIndex) }; }; var consumeTextToken = (plan, tokenIndex, tokens, renderEndTokenIndex, options) => { const { token, tokenText, hasBreakAfter, lastWordHighlightIndex, nextWordHighlightIndex } = options; const fallbackHighlightIndex = lastWordHighlightIndex ?? (hasFutureWordToken(tokens, tokenIndex + 1, renderEndTokenIndex) ? nextWordHighlightIndex : void 0); const textParts = tokenText.match(TEXT_TOKEN_SLICE_RE) ?? [tokenText]; for (const textPart of textParts) pushTextPart(plan, textPart, token.style, { highlightIndex: isPunctuationOnly(textPart) ? fallbackHighlightIndex : void 0 }); if (hasBreakAfter) { plan.push({ kind: "break" }); return skipWhitespaceTokens(tokens, tokenIndex + 1, renderEndTokenIndex); } return tokenIndex + 1; }; /** * Build a render plan for subtitle tokens preserving existing grouping rules. * * Important detail: leading punctuation before a word (for example "(" or "\"") * should be visually highlighted together with that word. */ function buildSubtitleRenderPlan(tokens, renderEndTokenIndex, breakAfterTokenIndexSet) { const plan = []; let wordHighlightIndex = 0; let lastWordHighlightIndex = null; for (let i = 0; i <= renderEndTokenIndex;) { const token = tokens[i]; const tokenText = token?.text ?? ""; if (!tokenText) { i += 1; continue; } if (tokenText === "\n") { plan.push({ kind: "break" }); i += 1; continue; } if (token.isWordLike) { const result = consumeWordToken(plan, tokens, i, renderEndTokenIndex, breakAfterTokenIndexSet, wordHighlightIndex); i = result.nextTokenIndex; if (result.consumedWord) { lastWordHighlightIndex = wordHighlightIndex; wordHighlightIndex += 1; } continue; } const hasBreakAfter = Boolean(breakAfterTokenIndexSet?.has(i)); i = consumeTextToken(plan, i, tokens, renderEndTokenIndex, { token, tokenText, hasBreakAfter, lastWordHighlightIndex, nextWordHighlightIndex: wordHighlightIndex }); } return plan; } //#endregion //#region src/subtitles/smartLayout.ts var roundToInt = (value) => Math.round(value); var resolveAspectBand = (aspect) => { if (aspect < .8) return { widthRatio: .9, charsPerLine: 27, fontHeightRatio: .03 }; if (aspect < 1.1) return { widthRatio: .84, charsPerLine: 31, fontHeightRatio: .031 }; if (aspect < 1.5) return { widthRatio: .76, charsPerLine: 36, fontHeightRatio: .033 }; if (aspect < 1.95) return { widthRatio: .72, charsPerLine: 40, fontHeightRatio: .034 }; return { widthRatio: .68, charsPerLine: 44, fontHeightRatio: .035 }; }; var resolveWidthBoost = (width) => { if (width >= 1920) return { extraChars: 4, widthScale: 1.04 }; if (width >= 1440) return { extraChars: 3, widthScale: 1.03 }; if (width >= 960) return { extraChars: 2, widthScale: 1.02 }; if (width >= 640) return { extraChars: 1, widthScale: 1.01 }; return { extraChars: 0, widthScale: 1 }; }; var estimateAverageGlyphWidth = (fontSizePx) => Math.max(7, fontSizePx * .56); function computeSmartLayoutForBox(box, cssMetrics = null) { const width = Number.isFinite(box.w) ? Math.max(0, box.w) : 0; const height = Number.isFinite(box.h) ? Math.max(0, box.h) : 0; if (width <= 0 || height <= 0) return { fontSizePx: cssMetrics?.fontSizePx ?? 20, maxWidthPx: cssMetrics?.maxWidthPx ?? null }; const { widthRatio, charsPerLine, fontHeightRatio } = resolveAspectBand(width / height); const { extraChars, widthScale } = resolveWidthBoost(width); const derivedFontSizePx = clampNumber(height * fontHeightRatio, 16, 42); const fontSizePx = cssMetrics?.fontSizePx ?? derivedFontSizePx; const averageGlyphWidth = estimateAverageGlyphWidth(fontSizePx); const minWidthPx = width * Math.min(.92, widthRatio); const maxWidthPx = width * clampNumber(widthRatio * widthScale, .66, .92); return { fontSizePx, maxWidthPx: roundToInt(clampNumber(clampNumber(charsPerLine + extraChars, 25, 48) * averageGlyphWidth, minWidthPx, maxWidthPx)) }; } //#endregion //#region src/subtitles/smartWrap.ts var STRONG_BREAK_RE = /[.!?…:;][)"'\]»”]*\s*$/u; var SOFT_BREAK_RE = /[,،、][)"'\]»”]*\s*$/u; var WHITESPACE_CHAR_RE = /\s/u; var DISCOURAGED_LINE_START_CHAR_RE = /^[\p{Pe}\p{Pf},.;:!?%\u2030\u2026]$/u; var DISCOURAGED_LINE_END_CHAR_RE = /^[\p{Ps}\p{Pi}\u00BF\u00A1([{\u00AB\u201C"'`-]$/u; var normalizeTokenText = (text) => text.replaceAll(/\s+/gu, " ").trim(); var getNextChar = (text, index) => { if (index >= text.length) return null; const codePoint = text.codePointAt(index); if (codePoint === void 0) return null; const char = String.fromCodePoint(codePoint); return { char, nextIndex: index + char.length }; }; var getPreviousChar = (text, index) => { if (index <= 0) return null; let start = index - 1; const lastCodeUnit = text.charCodeAt(start); if (lastCodeUnit >= 56320 && lastCodeUnit <= 57343 && start > 0) start -= 1; return { char: text.slice(start, index), previousIndex: start }; }; var isWhitespaceChar = (char) => WHITESPACE_CHAR_RE.test(char); var buildTokenTextBuffer = (tokens) => { const offsets = new Array(tokens.length + 1); offsets[0] = 0; let fullText = ""; for (let index = 0; index < tokens.length; index += 1) { fullText += tokens[index]?.text ?? ""; offsets[index + 1] = fullText.length; } return { fullText, offsets }; }; var getBufferedTokenText = (buffer, startToken, endToken) => { if (endToken <= startToken) return ""; return buffer.fullText.slice(buffer.offsets[startToken], buffer.offsets[endToken]); }; var resolveBoundary = (text) => { if (STRONG_BREAK_RE.test(text)) return "strong"; if (SOFT_BREAK_RE.test(text)) return "soft"; return "neutral"; }; var rangeStartsWithDiscouragedLineStart = (buffer, startToken, endToken) => { let index = buffer.offsets[startToken]; const end = buffer.offsets[endToken]; while (index < end) { const next = getNextChar(buffer.fullText, index); if (!next) return false; if (!isWhitespaceChar(next.char)) return DISCOURAGED_LINE_START_CHAR_RE.test(next.char); index = next.nextIndex; } return false; }; var rangeEndsWithDiscouragedLineEnd = (buffer, startToken, endToken) => { const start = buffer.offsets[startToken]; let index = buffer.offsets[endToken]; while (index > start) { const previous = getPreviousChar(buffer.fullText, index); if (!previous) return false; if (!isWhitespaceChar(previous.char)) return DISCOURAGED_LINE_END_CHAR_RE.test(previous.char); index = previous.previousIndex; } return false; }; var isWordToken = (token) => Boolean(token?.isWordLike && token.text.trim()); var getTokenStartMs = (token) => token && Number.isFinite(token.startMs) ? token.startMs : 0; var getTokenEndMs = (token) => token ? getTokenStartMs(token) + Math.max(0, token.durationMs) : 0; var getRangeStartMs = (tokens, start, end) => { for (let index = start; index < end; index += 1) { const token = tokens[index]; if (isWordToken(token)) return getTokenStartMs(token); } return getTokenStartMs(tokens[start]); }; var getRangeEndMs = (tokens, start, end) => { for (let index = end - 1; index >= start; index -= 1) { const token = tokens[index]; if (isWordToken(token)) return getTokenEndMs(token); } return getTokenEndMs(tokens[end - 1]); }; var createForcedBreakSlice = (tokens, tokenIndex) => { const token = tokens[tokenIndex]; const startMs = getTokenStartMs(token); return { text: "\n", tokenIndex, breakAfterTokenIndex: tokenIndex, startToken: tokenIndex, endToken: tokenIndex + 1, charLength: 0, startMs, endMs: startMs, boundary: "strong", forcesLineBreak: true }; }; var buildSliceFromWord = (tokens, wordTokenIndex, textBuffer, includeMetrics) => { let startToken = wordTokenIndex; while (startToken > 0 && tokens[startToken - 1]?.text !== "\n" && !isWordToken(tokens[startToken - 1])) startToken -= 1; let endToken = wordTokenIndex + 1; while (endToken < tokens.length && tokens[endToken]?.text !== "\n" && !isWordToken(tokens[endToken])) endToken += 1; const text = getBufferedTokenText(textBuffer, startToken, endToken); return { text, tokenIndex: wordTokenIndex, breakAfterTokenIndex: endToken - 1, startToken, endToken, charLength: includeMetrics ? normalizeTokenText(text).length : 0, startMs: includeMetrics ? getRangeStartMs(tokens, startToken, endToken) : 0, endMs: includeMetrics ? getRangeEndMs(tokens, startToken, endToken) : 0, boundary: resolveBoundary(text), forcesLineBreak: false }; }; var appendNextWordSlice = (slices, keyParts, tokens, textBuffer, collectKey, index) => { const token = tokens[index]; if (!token?.text) return index + 1; if (token.text === "\n") { const slice = createForcedBreakSlice(tokens, index); slices.push(slice); keyParts?.push("\n"); return index + 1; } if (!isWordToken(token)) return index + 1; const slice = buildSliceFromWord(tokens, index, textBuffer, collectKey); slices.push(slice); keyParts?.push(normalizeTokenText(slice.text)); return slice.breakAfterTokenIndex + 1; }; function buildWordSlicesFromBuffer(tokens, textBuffer, collectKey) { const slices = []; const keyParts = collectKey ? [] : null; let index = 0; while (index < tokens.length) index = appendNextWordSlice(slices, keyParts, tokens, textBuffer, collectKey, index); return { slices, key: keyParts?.join("|") ?? "" }; } function buildWordSlices(tokens) { return buildWordSlicesFromBuffer(tokens, buildTokenTextBuffer(tokens), true); } function measureWordSlices(wordSlices, measureText) { return wordSlices.map((slice) => ({ ...slice, width: slice.forcesLineBreak ? 0 : measureText(slice.text) })); } var getSegmentEndMs = (tokens, endTokenExclusive) => { if (endTokenExclusive <= 0) return 0; return getTokenEndMs(tokens[endTokenExclusive - 1]); }; var finalizeSegment = (out, tokens, startToken, endToken) => { if (endToken <= startToken) return; const startMs = getRangeStartMs(tokens, startToken, endToken); const endMs = getSegmentEndMs(tokens, endToken); out.push({ startToken, endToken, startMs, endMs: Math.max(startMs, endMs) }); }; var createSegmentBuildState = (metrics) => ({ segmentStartToken: metrics[0].startToken, segmentCharLength: 0, currentLineWidth: 0, currentLineCount: 1, lastTokenInSegment: metrics[0].startToken }); var handleForcedBreakMetric = (state, segments, tokens, metric) => { state.currentLineCount += 1; state.currentLineWidth = 0; state.lastTokenInSegment = metric.endToken; if (state.currentLineCount > 2) { const splitToken = Math.max(metric.startToken, metric.tokenIndex); finalizeSegment(segments, tokens, state.segmentStartToken, splitToken); state.segmentStartToken = splitToken; state.segmentCharLength = 0; state.lastTokenInSegment = state.segmentStartToken; } }; var appendMetricToCurrentLine = (state, metric, nextCharLength) => { state.currentLineWidth += metric.width; state.segmentCharLength = nextCharLength; state.lastTokenInSegment = metric.endToken; }; var startSecondLineWithMetric = (state, metric, nextCharLength) => { state.currentLineCount = 2; state.currentLineWidth = metric.width; state.segmentCharLength = nextCharLength; state.lastTokenInSegment = metric.endToken; }; var startNewSegmentWithMetric = (state, segments, tokens, metric) => { const splitToken = Math.max(metric.startToken, metric.tokenIndex); finalizeSegment(segments, tokens, state.segmentStartToken, splitToken); state.segmentStartToken = splitToken; state.segmentCharLength = metric.charLength; state.currentLineWidth = metric.width; state.currentLineCount = 1; state.lastTokenInSegment = metric.endToken; }; var handleMeasuredWordMetric = (state, segments, tokens, metric, maxWidth, charBudget) => { const nextCharLength = state.segmentCharLength + metric.charLength; if ((state.currentLineWidth === 0 || state.currentLineWidth + metric.width <= maxWidth) && nextCharLength <= charBudget) { appendMetricToCurrentLine(state, metric, nextCharLength); return; } if (state.currentLineCount === 1) { startSecondLineWithMetric(state, metric, nextCharLength); return; } startNewSegmentWithMetric(state, segments, tokens, metric); }; var alignSegmentEndTimes = (segments) => { for (let index = 0; index < segments.length - 1; index += 1) { const current = segments[index]; const next = segments[index + 1]; if (next.startMs > current.startMs) current.endMs = next.startMs; } }; var extendLastSegmentEndTime = (tokens, segments) => { const last = segments.at(-1); if (!last) return; last.endMs = Math.max(last.endMs, getRangeEndMs(tokens, last.startToken, last.endToken)); }; var finalizeComputedSegments = (tokens, segments) => { alignSegmentEndTimes(segments); extendLastSegmentEndTime(tokens, segments); return segments.filter((segment) => segment.endToken > segment.startToken); }; function computeTwoLineSegments(tokens, metrics, maxWidthPx, maxLength) { if (!metrics.length || !tokens.length) return []; const maxWidth = Math.max(1, Number.isFinite(maxWidthPx) ? maxWidthPx : 0); const charBudget = Math.max(1, Number.isFinite(maxLength) ? maxLength : 0); const segments = []; const state = createSegmentBuildState(metrics); for (const metric of metrics) { if (metric.forcesLineBreak) { handleForcedBreakMetric(state, segments, tokens, metric); continue; } handleMeasuredWordMetric(state, segments, tokens, metric, maxWidth, charBudget); } finalizeSegment(segments, tokens, state.segmentStartToken, state.lastTokenInSegment); return finalizeComputedSegments(tokens, segments); } var measureTokenRange = (textBuffer, startToken, endToken, measureText) => { if (endToken <= startToken) return 0; return measureText(getBufferedTokenText(textBuffer, startToken, endToken)); }; var scoreBreakCandidate = ({ firstWidth, secondWidth, lineStartPenalty, lineEndPenalty, firstWordCount, secondWordCount, maxWidthPx, boundary }) => { const overflowPenalty = Math.max(0, firstWidth - maxWidthPx) * 12 + Math.max(0, secondWidth - maxWidthPx) * 12; const balancePenalty = Math.abs(secondWidth / Math.max(firstWidth, 1) - 1.08) * 120; const shortTopPenalty = firstWordCount < 2 ? 80 : 0; const orphanPenalty = secondWordCount < 2 ? 80 : 0; let boundaryBonus = 0; if (boundary === "strong") boundaryBonus = -28; else if (boundary === "soft") boundaryBonus = -14; return overflowPenalty + balancePenalty + shortTopPenalty + orphanPenalty + lineStartPenalty + lineEndPenalty + boundaryBonus; }; var emptyTokenWrapPlan = () => ({ breakAfterTokenIndices: [] }); var singleBreakTokenWrapPlan = (breakAfterTokenIndex) => ({ breakAfterTokenIndices: [breakAfterTokenIndex] }); var hasForcedLineBreakToken = (tokens) => tokens.some((token) => token.text === "\n"); var findBestWordBreakAfterTokenIndex = (tokens, textBuffer, measurableSlices, measureText, maxWidthPx) => { let bestBreakAfterTokenIndex = null; let bestScore = Number.POSITIVE_INFINITY; for (let index = 0; index < measurableSlices.length - 1; index += 1) { const slice = measurableSlices[index]; const nextSlice = measurableSlices[index + 1]; const candidateBreakAfterTokenIndex = Math.max(slice.breakAfterTokenIndex, nextSlice.tokenIndex - 1); const firstEndToken = candidateBreakAfterTokenIndex + 1; const secondStartToken = nextSlice.tokenIndex; const score = scoreBreakCandidate({ firstWidth: measureTokenRange(textBuffer, 0, firstEndToken, measureText), secondWidth: measureTokenRange(textBuffer, secondStartToken, tokens.length, measureText), lineStartPenalty: rangeStartsWithDiscouragedLineStart(textBuffer, secondStartToken, tokens.length) ? 260 : 0, lineEndPenalty: rangeEndsWithDiscouragedLineEnd(textBuffer, 0, firstEndToken) ? 70 : 0, firstWordCount: index + 1, secondWordCount: measurableSlices.length - (index + 1), maxWidthPx, boundary: slice.boundary }); if (score < bestScore) { bestScore = score; bestBreakAfterTokenIndex = candidateBreakAfterTokenIndex; } } return bestBreakAfterTokenIndex; }; function computeTokenWrapPlan(tokens, measureText, maxWidthPx) { if (!tokens.length || hasForcedLineBreakToken(tokens)) return emptyTokenWrapPlan(); const safeMaxWidthPx = Number.isFinite(maxWidthPx) ? maxWidthPx : 0; if (safeMaxWidthPx <= 0) return emptyTokenWrapPlan(); const textBuffer = buildTokenTextBuffer(tokens); const { slices } = buildWordSlicesFromBuffer(tokens, textBuffer, false); const measurableSlices = slices.filter((slice) => !slice.forcesLineBreak); if (!measurableSlices.length) return emptyTokenWrapPlan(); if (measureTokenRange(textBuffer, 0, tokens.length, measureText) <= safeMaxWidthPx) return emptyTokenWrapPlan(); const bestBreakAfterTokenIndex = findBestWordBreakAfterTokenIndex(tokens, textBuffer, measurableSlices, measureText, safeMaxWidthPx); if (bestBreakAfterTokenIndex !== null) return singleBreakTokenWrapPlan(bestBreakAfterTokenIndex); return emptyTokenWrapPlan(); } //#endregion //#region src/shims/rvfc-polyfill.ts var VidProto = typeof HTMLVideoElement === "undefined" ? {} : HTMLVideoElement.prototype; var hasQuality = "getVideoPlaybackQuality" in VidProto || "webkitDecodedFrameCount" in VidProto || "mozPresentedFrames" in VidProto || "mozPaintedFrames" in VidProto; if (!("requestVideoFrameCallback" in VidProto) && hasQuality && typeof requestAnimationFrame === "function") { VidProto._rvfcpolyfillmap = {}; const getPlaybackQuality = "getVideoPlaybackQuality" in VidProto ? (video) => { const quality = video.getVideoPlaybackQuality(); const totalFrameDelay = quality.totalFrameDelay ?? 0; return { presentedFrames: quality.totalVideoFrames - quality.droppedVideoFrames, totalFrameDelay }; } : (video) => ({ presentedFrames: video.mozPresentedFrames ?? video.mozPaintedFrames ?? (video.webkitDecodedFrameCount || 0) - (video.webkitDroppedFrameCount || 0), totalFrameDelay: video.mozFrameDelay || 0 }); VidProto.requestVideoFrameCallback = function(callback) { const handle = performance.now(); const quality = getPlaybackQuality(this); const baseline = quality.presentedFrames; const check = (old, now) => { const newquality = getPlaybackQuality(this); const presentedFrames = newquality.presentedFrames; if (presentedFrames > baseline) { const processingDuration = newquality.totalFrameDelay - quality.totalFrameDelay || 0; const timediff = now - old; callback(now, { presentationTime: now + processingDuration * 1e3, expectedDisplayTime: now + timediff, width: this.videoWidth, height: this.videoHeight, mediaTime: Math.max(0, this.currentTime || 0) + timediff / 1e3, presentedFrames, processingDuration }); delete this._rvfcpolyfillmap?.[handle]; } else { this._rvfcpolyfillmap ??= {}; this._rvfcpolyfillmap[handle] = requestAnimationFrame((newer) => check(now, newer)); } }; this._rvfcpolyfillmap ??= {}; this._rvfcpolyfillmap[handle] = requestAnimationFrame((newer) => check(handle, newer)); return handle; }; VidProto.cancelVideoFrameCallback = function(handle) { const rafHandle = this._rvfcpolyfillmap?.[handle]; if (rafHandle != null) { cancelAnimationFrame(rafHandle); delete this._rvfcpolyfillmap[handle]; } }; } //#endregion //#region src/subtitles/widget.ts var EDGE_PUNCTUATION_OR_SYMBOL_RE = /^[\p{P}\p{S}]$/u; var isEdgePunctuationOrSymbol = (char) => EDGE_PUNCTUATION_OR_SYMBOL_RE.test(char); var trimEdgePunctuation = (value) => { const chars = Array.from(value); let startIndex = 0; let endIndex = chars.length; while (startIndex < endIndex && isEdgePunctuationOrSymbol(chars[startIndex])) startIndex += 1; while (endIndex > startIndex && isEdgePunctuationOrSymbol(chars[endIndex - 1])) endIndex -= 1; return chars.slice(startIndex, endIndex).join(""); }; var WRAP_WIDTH_GUARD_PX = 8; var WRAP_WIDTH_GUARD_RATIO = .97; var MIN_EFFECTIVE_WRAP_WIDTH_PX = 24; function applyWrapWidthGuard(maxWidthPx) { if (!Number.isFinite(maxWidthPx) || maxWidthPx <= 0) return 0; const byPixelGuard = maxWidthPx - WRAP_WIDTH_GUARD_PX; const byRatioGuard = maxWidthPx * WRAP_WIDTH_GUARD_RATIO; return Math.max(MIN_EFFECTIVE_WRAP_WIDTH_PX, Math.min(byPixelGuard, byRatioGuard)); } var SubtitlesWidget = class { video; container; fullscreenLayerController; tooltipMount; subtitlesContainer = null; subtitlesBlock = null; renderedHighlightEls = []; passedFlagsBuffer = []; subtitles = null; subtitleLang; lastRenderKey = null; lastActiveLineKey = null; maxActiveCueLookbackMs = 0; highlightWords = false; fontSize = 20; fontSizeOverridden = false; fontFamily = "default-sans"; maxLength = 300; smartLayoutEnabled = true; smartFontSizePx = 0; smartMaxWidthPx = 0; smartAnchorWidthPx = 0; smartAnchorHeightPx = 0; lastSmartLayoutKey = null; lastSmartLayoutCheckTs = 0; opacity = "0.2"; repositionPending = false; positionRefreshPending = false; updatePending = false; lastUpdateRequestTs = 0; updateMinIntervalMs = 100; updateMinIntervalHighlightMs = 33; useVideoFrameCallbacks; videoFrameRequestId = null; lastPlaybackTimeMs = null; dragAbortController = null; lastPositionRefreshTs = 0; positionRefreshIntervalMs = 250; subtitleMaxWidthPx = 0; breakAfterTokenIndices = []; breakAfterTokenIndexSet = null; wrapPending = false; lastWrapKey = null; lastWrapTokens = null; measureCanvas = null; measureCtx = null; tokenProcessingMemo = null; tokenPrecomputeMemo = null; lineMeasureMemo = null; lastSegmentIndex = 0; lastAppliedLeftPct = null; lastAppliedTopPct = null; position = { left: 50, top: 100 }; customVerticalAnchorState = null; positionPreset = "bottom-center"; dragging = { pointerId: null, candidate: false, active: false, moved: false, startClientX: 0, startClientY: 0, offset: { x: 0, y: 0 } }; dragStartThresholdPx = 4; snapThresholdPx = 18; suppressTokenClicksUntil = 0; abortController = new AbortController(); resizeObserver; tokenTooltip; tooltipTranslationRequestId = 0; intervalIdleChecker; checkerUnsubscribe = null; strTokens = ""; strTranslatedTokens = ""; passedStateKey = null; passedThresholds = []; normalizeTokenTextForTranslation(raw) { return trimEdgePunctuation(raw.trim()); } bottomInsetCachedPx = 0; safeAreaBottomInsetCachedPx = 0; containerPaddingBottomCachedPx = 0; insetCacheReady = false; bottomInsetByMode = { normal: { ratio: .1, minPx: 56, maxPx: 220, gapPx: 10 }, fullscreen: { ratio: .07, minPx: 44, maxPx: 140, gapPx: 9 } }; safeAreaProbeEl = null; guidesLayer = null; verticalGuide = null; horizontalGuide = null; onPointerDownBound; onPointerUpBound; onPointerMoveBound; onPlaybackStateChangeBound; onVisualViewportChangeBound; constructor(video, container, intervalIdleChecker) { this.video = video; this.container = container; this.fullscreenLayerController = new FullscreenLayerController({ container }); this.intervalIdleChecker = intervalIdleChecker; this.useVideoFrameCallbacks = !!this.video && typeof this.video.requestVideoFrameCallback === "function"; this.onPointerDownBound = (event) => this.onPointerDown(event); this.onPointerUpBound = (event) => this.onPointerUp(event); this.onPointerMoveBound = (event) => this.onPointerMove(event); this.onPlaybackStateChangeBound = () => this.handlePlaybackStateChange(); this.onVisualViewportChangeBound = () => this.scheduleReposition(); this.checkerUnsubscribe = this.intervalIdleChecker.subscribe(() => { this.onCheckerTick(); }); this.bindEvents(); } updateMount({ container }) { const containerChanged = this.container !== container; this.container = container; this.fullscreenLayerController.updateContainer(container); this.syncWidgetMount(); if (containerChanged) { const parentElement = this.getTokenTooltipParentElement(); this.tokenTooltip?.updateMount({ parentElement }); } if (this.subtitles) { this.insetCacheReady = false; this.lastAppliedLeftPct = null; this.lastAppliedTopPct = null; this.updateContainerRect(); this.requestUpdate(); } } resetTranslationContext(releaseTooltip = false) { this.strTranslatedTokens = ""; if (releaseTooltip) this.releaseTooltip(); } resetSegmentationMemo() { this.tokenProcessingMemo = null; this.tokenPrecomputeMemo = null; this.lineMeasureMemo = null; this.lastSegmentIndex = 0; } resetWrapMemo() { this.setBreakAfterTokenIndices([]); this.lastWrapKey = null; } resetRenderMemo() { this.lastRenderKey = null; } computeAnchorBoxLayout(layout) { return { left: 0, top: 0, w: layout.w, h: layout.h }; } readSmartCssMetrics() { const block = this.subtitlesBlock; if (!block) return null; const cs = getComputedStyle(block); const fontSizePx = Number.parseFloat(cs.fontSize); const maxWidthRawPx = Number.parseFloat(cs.maxWidth); if (!Number.isFinite(fontSizePx) || !Number.isFinite(maxWidthRawPx) || fontSizePx <= 0 || maxWidthRawPx <= 0) return null; this.subtitleMaxWidthPx = maxWidthRawPx; const paddingLeft = Number.parseFloat(cs.paddingLeft) || 0; const paddingRight = Number.parseFloat(cs.paddingRight) || 0; const maxWidthPx = Math.max(0, maxWidthRawPx - paddingLeft - paddingRight); if (maxWidthPx <= 0) return null; return { fontSizePx, maxWidthPx }; } ensureSmartLayout(anchorBox) { if (!this.smartLayoutEnabled) return null; const cssMetrics = this.readSmartCssMetrics(); const nextFontSizePx = cssMetrics?.fontSizePx ?? this.smartFontSizePx; const next = computeSmartLayoutForBox(anchorBox, cssMetrics); const nextMaxWidthPx = next.maxWidthPx ?? this.smartMaxWidthPx; const nextKey = `${Math.round(nextFontSizePx)}|${Math.round(nextMaxWidthPx)}|${Math.round(next.maxWidthPx ?? 0)}`; const fontChanged = Math.abs(nextFontSizePx - this.smartFontSizePx) > .5; const widthChanged = Math.abs(nextMaxWidthPx - this.smartMaxWidthPx) > .5; if (nextKey !== this.lastSmartLayoutKey) { this.lastSmartLayoutKey = nextKey; this.smartFontSizePx = nextFontSizePx; this.smartMaxWidthPx = nextMaxWidthPx; this.resetRenderMemo(); } this.setSubtitlesContainerVar("--vot-subtitles-max-width", next.maxWidthPx && next.maxWidthPx > 0 ? `${next.maxWidthPx}px` : null); if ((fontChanged || widthChanged) && this.lastWrapTokens) { this.lastWrapKey = null; this.resetSegmentationMemo(); this.scheduleWrapRecompute(); } return next; } scheduleReposition() { if (this.abortController.signal.aborted) return; if (!this.subtitles) return; this.repositionPending = true; this.intervalIdleChecker.markActivity("subtitles-reposition"); this.intervalIdleChecker.requestImmediateTick(); } setSubtitlesContainerVar(name, value) { const container = this.subtitlesContainer; if (!container) return; if (value === null) { container.style.removeProperty(name); return; } container.style.setProperty(name, value); } applyOpacityStyle() { this.setSubtitlesContainerVar("--vot-subtitles-opacity", this.opacity); } applyManualFontSizeStyle() { if (!this.smartLayoutEnabled && this.fontSizeOverridden) { this.setSubtitlesContainerVar("--vot-subtitles-font-size", `${this.fontSize}px`); return; } this.setSubtitlesContainerVar("--vot-subtitles-font-size", null); } applyFontFamilyStyle() { const fontFamily = this.fontFamily; this.setSubtitlesContainerVar("--vot-subtitles-font-family-custom", getSubtitleFontFamilyCssValue(fontFamily)); ensureGoogleSubtitleFontLoaded(fontFamily, { forceGmXhr: true, onLoaded: () => { if (this.fontFamily !== fontFamily) return; this.lastWrapKey = null; this.resetSegmentationMemo(); this.scheduleWrapRecompute(); this.scheduleReposition(); } }); } syncVisualStyleVars() { this.applyOpacityStyle(); this.applyManualFontSizeStyle(); this.applyFontFamilyStyle(); } ensureGuidesLayer() { if (this.guidesLayer) return this.guidesLayer; const layer = document.createElement("vot-block"); layer.classList.add("vot-subtitles-guides"); const verticalGuide = document.createElement("vot-block"); verticalGuide.classList.add("vot-subtitles-guide", "vot-subtitles-guide--vertical"); const horizontalGuide = document.createElement("vot-block"); horizontalGuide.classList.add("vot-subtitles-guide", "vot-subtitles-guide--horizontal"); layer.append(verticalGuide, horizontalGuide); this.guidesLayer = layer; this.verticalGuide = verticalGuide; this.horizontalGuide = horizontalGuide; this.hideSnapGuides(); return layer; } hideSnapGuides() { this.verticalGuide?.removeAttribute("data-visible"); this.horizontalGuide?.removeAttribute("data-visible"); } updateSnapGuides(anchorBox, options) { const { showVerticalCenter = false, showHorizontalCenter = false } = options; if (!this.ensureGuidesLayer().isConnected) this.syncGuideLayerMount(); if (this.verticalGuide) { this.verticalGuide.style.left = `${anchorBox.left + anchorBox.w / 2}px`; this.verticalGuide.style.top = `${anchorBox.top}px`; this.verticalGuide.style.height = `${anchorBox.h}px`; if (showVerticalCenter) this.verticalGuide.dataset.visible = "true"; else delete this.verticalGuide.dataset.visible; } if (this.horizontalGuide) { this.horizontalGuide.style.left = `${anchorBox.left}px`; this.horizontalGuide.style.top = `${anchorBox.top + anchorBox.h / 2}px`; this.horizontalGuide.style.width = `${anchorBox.w}px`; if (showHorizontalCenter) this.horizontalGuide.dataset.visible = "true"; else delete this.horizontalGuide.dataset.visible; } } syncGuideLayerMount() { const guidesLayer = this.ensureGuidesLayer(); if (guidesLayer.parentElement !== this.container) this.container.appendChild(guidesLayer); } syncWidgetMount() { this.fullscreenLayerController.syncWidgetContainer(null); if (this.subtitlesContainer && this.subtitlesContainer.parentElement !== this.container) this.container.appendChild(this.subtitlesContainer); if (this.tooltipMount) reparentShadowMount(this.tooltipMount, this.container); this.syncGuideLayerMount(); } ensureTooltipMount() { if (this.tooltipMount) reparentShadowMount(this.tooltipMount, this.container); else this.tooltipMount = createShadowMount({ parent: this.container, rootClasses: ["vot-portal-local"], hostStyles: { position: "absolute", inset: "0", display: "block", "pointer-events": "none" }, rootStyles: { position: "relative", display: "block", width: "100%", height: "100%", "pointer-events": "none" } }); return this.tooltipMount; } getTokenTooltipParentElement() { return this.ensureTooltipMount().root; } createSubtitlesContainer() { if (this.subtitlesContainer) return this.subtitlesContainer; const container = document.createElement("vot-block"); container.classList.add("vot-subtitles-widget"); this.subtitlesContainer = container; this.syncWidgetMount(); container.addEventListener("pointerdown", this.onPointerDownBound, { signal: this.abortController.signal, passive: false, capture: true }); this.syncVisualStyleVars(); this.insetCacheReady = false; this.updateContainerRect(); return container; } onGlobalPointerDown = (event) => { const eventPath = typeof event.composedPath === "function" ? event.composedPath() : []; if (this.tokenTooltip?.container && (this.tokenTooltip.container.contains(event.target) || eventPath.includes(this.tokenTooltip.container))) return; if (this.subtitlesContainer && !this.subtitlesContainer.contains(event.target)) this.releaseTooltip(); }; bindEvents() { const { signal } = this.abortController; const opts = { signal }; for (const eventName of [ "play", "pause", "seeking", "seeked", "ended" ]) this.video?.addEventListener(eventName, this.onPlaybackStateChangeBound, opts); this.resizeObserver = new ResizeObserver(() => this.onResize()); const resizeTarget = this.container instanceof ShadowRoot ? this.container.host : this.container; this.resizeObserver.observe(resizeTarget); if (this.video) this.resizeObserver.observe(this.video); globalThis.visualViewport?.addEventListener("resize", this.onVisualViewportChangeBound, opts); globalThis.visualViewport?.addEventListener("scroll", this.onVisualViewportChangeBound, opts); globalThis.addEventListener("pointerdown", this.onGlobalPointerDown); } getUpdateMinIntervalMs() { return this.highlightWords ? this.updateMinIntervalHighlightMs : this.updateMinIntervalMs; } requestUpdate(playbackTimeMs, now = performance.now()) { if (this.abortController.signal.aborted) return; if (!this.subtitles) return; if (typeof playbackTimeMs === "number" && Number.isFinite(playbackTimeMs)) this.lastPlaybackTimeMs = Math.max(0, playbackTimeMs); else if (this.video) this.lastPlaybackTimeMs = Math.max(0, this.video.currentTime * 1e3); const minInterval = this.getUpdateMinIntervalMs(); if (now - this.lastUpdateRequestTs < minInterval) return; this.lastUpdateRequestTs = now; this.updatePending = true; this.intervalIdleChecker.requestImmediateTick(); } resolvePlaybackTimeMs() { if (typeof this.lastPlaybackTimeMs === "number" && Number.isFinite(this.lastPlaybackTimeMs)) return this.lastPlaybackTimeMs; return this.video ? Math.max(0, this.video.currentTime * 1e3) : 0; } handlePlaybackStateChange() { if (!this.subtitles) { this.stopVideoFrameLoop(); return; } this.scheduleReposition(); this.requestUpdate(this.video ? Math.max(0, this.video.currentTime * 1e3) : 0); this.syncVideoFrameLoop(); } syncVideoFrameLoop() { if (!this.useVideoFrameCallbacks) return; const video = this.video; if (!video) return; if (!this.subtitles || video.paused || video.ended) { this.stopVideoFrameLoop(); return; } this.startVideoFrameLoop(); } startVideoFrameLoop() { if (!this.useVideoFrameCallbacks) return; const video = this.video; if (!video) return; if (this.videoFrameRequestId !== null) return; this.videoFrameRequestId = video.requestVideoFrameCallback(this.onVideoFrame); } stopVideoFrameLoop() { if (!this.useVideoFrameCallbacks) return; const video = this.video; if (!video) return; if (this.videoFrameRequestId === null) return; try { video.cancelVideoFrameCallback(this.videoFrameRequestId); } catch {} this.videoFrameRequestId = null; } onVideoFrame = (now, metadata) => { this.videoFrameRequestId = null; if (this.abortController.signal.aborted) return; const video = this.video; if (!video || video.paused || video.ended || !this.subtitles) return; const mediaTime = Number.isFinite(metadata.mediaTime) ? metadata.mediaTime : null; const rawTime = mediaTime === 0 && video.currentTime > 0 ? video.currentTime : mediaTime; const playbackTimeMs = rawTime == null ? void 0 : rawTime * 1e3; this.requestUpdate(playbackTimeMs, now); this.startVideoFrameLoop(); }; onCheckerTick() { if (this.abortController.signal.aborted) return; if (this.repositionPending) { this.repositionPending = false; this.updateContainerRect(); this.updatePending = true; } if (this.wrapPending) { this.wrapPending = false; this.recomputeWrapNow(); } if (this.positionRefreshPending) { this.positionRefreshPending = false; this.applySubtitlePosition(); } if (this.updatePending) { this.updatePending = false; this.update(); } } attachDragDocumentListeners() { if (this.dragAbortController) return; const dragAbortController = new AbortController(); const { signal } = dragAbortController; document.addEventListener("pointermove", this.onPointerMoveBound, { signal, passive: false, capture: true }); document.addEventListener("pointerup", this.onPointerUpBound, { signal, capture: true }); document.addEventListener("pointercancel", this.onPointerUpBound, { signal, capture: true }); this.dragAbortController = dragAbortController; } detachDragDocumentListeners() { this.dragAbortController?.abort(); this.dragAbortController = null; } onResize() { this.syncWidgetMount(); this.scheduleReposition(); } updateContainerRect() { const layout = this.getLayoutSize(); if (!layout.w || !layout.h) return; const anchorBox = this.computeAnchorBoxLayout(layout); if (!anchorBox.w || !anchorBox.h) return; this.refreshBottomInsetNow(layout, anchorBox); this.applySubtitlePositionWithLayout(layout, anchorBox); } getLayoutSize() { const layoutRoot = this.fullscreenLayerController.getLayoutRootElement(); const rect = layoutRoot.getBoundingClientRect(); const w = layoutRoot.clientWidth || rect.width; const h = layoutRoot.clientHeight || rect.height; return { w, h, rect, scaleX: rect.width && w ? rect.width / w : 1, scaleY: rect.height && h ? rect.height / h : 1 }; } ensureSafeAreaProbe() { if (this.safeAreaProbeEl) return; const el = document.createElement("div"); el.style.position = "fixed"; el.style.left = "0"; el.style.right = "0"; el.style.bottom = "0"; el.style.height = "env(safe-area-inset-bottom, 0px)"; el.style.pointerEvents = "none"; el.style.opacity = "0"; el.style.zIndex = "-1"; document.documentElement.appendChild(el); this.safeAreaProbeEl = el; } getSafeAreaBottomInsetPx() { this.ensureSafeAreaProbe(); if (!this.safeAreaProbeEl) return 0; return this.safeAreaProbeEl.offsetHeight || 0; } refreshInsetCache() { const layoutRoot = this.fullscreenLayerController.getLayoutRootElement(); this.safeAreaBottomInsetCachedPx = this.getSafeAreaBottomInsetPx(); this.containerPaddingBottomCachedPx = Number.parseFloat(getComputedStyle(layoutRoot).paddingBottom || "0") || 0; this.insetCacheReady = true; } isMobileViewport() { return globalThis.matchMedia?.("(max-width: 900px) and (pointer: coarse)")?.matches ?? false; } getBottomInsetPreset() { const doc = document; const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; if (!(fullscreenEl instanceof Element)) return this.bottomInsetByMode.normal; const { container, video } = this; if (fullscreenEl === container || fullscreenEl.contains(container) || container.contains(fullscreenEl)) return this.bottomInsetByMode.fullscreen; if (video && (fullscreenEl === video || fullscreenEl.contains(video) || video.contains(fullscreenEl))) return this.bottomInsetByMode.fullscreen; return this.bottomInsetByMode.normal; } computeReservedBottomInsetPx(anchorBoxH, preset = this.getBottomInsetPreset()) { return clampToRange(anchorBoxH * preset.ratio, preset.minPx, preset.maxPx); } refreshBottomInsetNow(layout, anchorBox) { this.refreshInsetCache(); const anchorH = anchorBox?.h ?? this.computeAnchorBoxLayout(layout ?? this.getLayoutSize()).h; if (!anchorH) { this.bottomInsetCachedPx = 0; return; } const preset = this.getBottomInsetPreset(); this.bottomInsetCachedPx = this.computeReservedBottomInsetPx(anchorH, preset); } getBottomInsetPx(layout, anchorBox) { if (!this.insetCacheReady) this.refreshInsetCache(); const preset = this.getBottomInsetPreset(); const safeAreaBottom = this.safeAreaBottomInsetCachedPx; const paddingBottom = this.containerPaddingBottomCachedPx; if (this.isMobileViewport()) return Math.max(paddingBottom, safeAreaBottom); const anchorH = anchorBox?.h ?? this.computeAnchorBoxLayout(layout ?? this.getLayoutSize()).h; const reserved = anchorH ? this.computeReservedBottomInsetPx(anchorH, preset) : preset.minPx; const stableInset = Math.max(this.bottomInsetCachedPx, reserved); return Math.max(paddingBottom, safeAreaBottom, stableInset) + preset.gapPx; } onPointerDown(event) { const subtitlesContainer = this.subtitlesContainer; if (!subtitlesContainer) return; const target = event.target; if (!(target instanceof Node) || !subtitlesContainer.contains(target)) return; if (!event.isPrimary) return; if (event.pointerType === "mouse" && event.button !== 0) return; event.stopPropagation(); const layout = this.getLayoutSize(); const { rect: containerRect, w, h, scaleX, scaleY } = layout; if (!w || !h) return; const anchorBox = this.computeAnchorBoxLayout(layout); if (!anchorBox.w || !anchorBox.h) return; this.lastPositionRefreshTs = performance.now(); const subRect = subtitlesContainer.getBoundingClientRect(); const pointerX = (event.clientX - containerRect.left) / scaleX - anchorBox.left; const pointerY = (event.clientY - containerRect.top) / scaleY - anchorBox.top; const anchorX = (subRect.left - containerRect.left + subRect.width / 2) / scaleX - anchorBox.left; const anchorY = (subRect.top - containerRect.top + subRect.height) / scaleY - anchorBox.top; this.dragging.pointerId = event.pointerId; this.dragging.candidate = true; this.dragging.active = false; this.dragging.moved = false; this.dragging.startClientX = event.clientX; this.dragging.startClientY = event.clientY; this.dragging.offset.x = anchorX - pointerX; this.dragging.offset.y = anchorY - pointerY; this.hideSnapGuides(); this.attachDragDocumentListeners(); } onPointerUp(event) { if (this.dragging.pointerId === null) return; if (event.pointerId !== this.dragging.pointerId) return; if (this.dragging.moved) this.suppressTokenClicksUntil = performance.now() + 450; this.dragging.pointerId = null; this.dragging.candidate = false; this.dragging.active = false; this.dragging.moved = false; this.hideSnapGuides(); this.detachDragDocumentListeners(); } onPointerMove(event) { if (!this.dragging.candidate || this.dragging.pointerId === null) return; if (event.pointerId !== this.dragging.pointerId) return; if (this.dragging.active) this.dragging.moved = true; else { if (!hasDragThresholdBeenExceeded(this.dragging.startClientX, this.dragging.startClientY, event.clientX, event.clientY, this.dragStartThresholdPx)) return; this.dragging.active = true; this.dragging.moved = true; this.suppressTokenClicksUntil = performance.now() + 450; try { this.subtitlesContainer?.setPointerCapture(event.pointerId); } catch {} } event.preventDefault(); event.stopPropagation(); const layout = this.getLayoutSize(); const { rect: containerRect, w, h, scaleX, scaleY } = layout; if (!w || !h) return; const anchorBox = this.computeAnchorBoxLayout(layout); if (!anchorBox.w || !anchorBox.h) return; const pointerX = (event.clientX - containerRect.left) / scaleX - anchorBox.left; const pointerY = (event.clientY - containerRect.top) / scaleY - anchorBox.top; let anchorX = pointerX + this.dragging.offset.x; let anchorY = pointerY + this.dragging.offset.y; const elW = this.subtitlesContainer?.offsetWidth ?? 0; const elH = this.subtitlesContainer?.offsetHeight ?? 0; const bottomInset = 0; const snappedX = snapValueToNearestCandidate({ current: anchorX, candidates: [anchorBox.w / 2], thresholdPx: this.snapThresholdPx }); if (snappedX.snapped) anchorX = snappedX.value; const verticalCenterAnchor = anchorBox.h / 2 + elH / 2; const snappedY = snapValueToNearestCandidate({ current: anchorY, candidates: [verticalCenterAnchor], thresholdPx: this.snapThresholdPx }); if (snappedY.snapped) anchorY = snappedY.value; ({anchorX, anchorY} = clampAnchorWithinBox({ anchorX, anchorY, elementWidth: elW, elementHeight: elH, boxWidth: anchorBox.w, boxHeight: anchorBox.h, bottomInset })); this.positionPreset = "custom"; this.customVerticalAnchorState = captureCustomVerticalAnchorState({ anchorY, elementHeight: elH, boxHeight: anchorBox.h, bottomInset }); this.position.left = anchorX / anchorBox.w * 100; this.position.top = anchorY / anchorBox.h * 100; this.updateSnapGuides(anchorBox, { showVerticalCenter: snappedX.snapped, showHorizontalCenter: snappedY.snapped }); this.applySubtitlePositionWithLayout(layout, anchorBox); } applySubtitlePosition() { if (!this.subtitlesContainer) return; const layout = this.getLayoutSize(); if (!layout.w || !layout.h) return; const anchorBox = this.computeAnchorBoxLayout(layout); if (!anchorBox.w || !anchorBox.h) return; this.applySubtitlePositionWithLayout(layout, anchorBox); } applySubtitlePositionWithLayout(layout, anchorBox) { const subtitlesContainer = this.subtitlesContainer; if (!subtitlesContainer) return; this.applyScaleCompensation(subtitlesContainer, layout); this.syncAnchorDimensions(subtitlesContainer, anchorBox); if (this.smartLayoutEnabled) this.ensureSmartLayout(anchorBox); const elW = subtitlesContainer.offsetWidth; const elH = subtitlesContainer.offsetHeight; const bottomInset = this.positionPreset === "custom" ? 0 : this.getBottomInsetPx(layout, anchorBox); const anchorPosition = this.resolveCurrentAnchorPosition(anchorBox, elW, elH, bottomInset); const containerPosition = this.clampContainerPosition(anchorBox, anchorPosition.anchorX, anchorPosition.anchorY, elW, elH, bottomInset); const anchorX = containerPosition.anchorX; const anchorY = containerPosition.anchorY; const containerAnchorX = anchorBox.left + anchorX; const containerAnchorY = anchorBox.top + anchorY; const leftPct = containerAnchorX / layout.w * 100; const topPct = containerAnchorY / layout.h * 100; this.updateContainerPosition(subtitlesContainer, leftPct, topPct); this.tokenTooltip?.updatePos(); } applyScaleCompensation(subtitlesContainer, layout) { const visualScale = Math.min(layout.scaleX || 1, layout.scaleY || 1); const compensate = visualScale > 0 && visualScale < .999 ? Math.min(1 / visualScale, 3) : 1; if (Math.abs(compensate - 1) < .001) { subtitlesContainer.style.removeProperty("--vot-subtitles-scale-compensation"); return; } subtitlesContainer.style.setProperty("--vot-subtitles-scale-compensation", compensate.toFixed(3)); } syncAnchorDimensions(subtitlesContainer, anchorBox) { const anchorWidthPx = Math.max(1, Math.round(anchorBox.w)); const anchorHeightPx = Math.max(1, Math.round(anchorBox.h)); if (!(anchorWidthPx !== this.smartAnchorWidthPx || anchorHeightPx !== this.smartAnchorHeightPx)) return; this.smartAnchorWidthPx = anchorWidthPx; this.smartAnchorHeightPx = anchorHeightPx; subtitlesContainer.style.setProperty("--vot-subtitles-anchor-width", `${anchorWidthPx}px`); subtitlesContainer.style.setProperty("--vot-subtitles-anchor-height", `${anchorHeightPx}px`); if (this.lastWrapTokens) { this.lastWrapKey = null; this.resetSegmentationMemo(); this.scheduleWrapRecompute(); } } resolveCurrentAnchorPosition(anchorBox, elementWidth, elementHeight, bottomInset) { let anchorX = this.position.left / 100 * anchorBox.w; let anchorY = this.position.top / 100 * anchorBox.h; if (this.positionPreset === "custom") { anchorY = resolveCustomVerticalAnchor({ state: this.customVerticalAnchorState, elementHeight, boxHeight: anchorBox.h, bottomInset }); return { anchorX, anchorY }; } const presetPosition = this.resolvePresetAnchorPosition({ preset: this.positionPreset, anchorBox, elementWidth, elementHeight, bottomInset }); anchorX = presetPosition.anchorX; anchorY = presetPosition.anchorY; if (anchorBox.w > 0) this.position.left = anchorX / anchorBox.w * 100; if (anchorBox.h > 0) this.position.top = anchorY / anchorBox.h * 100; return { anchorX, anchorY }; } clampContainerPosition(anchorBox, anchorX, anchorY, elementWidth, elementHeight, bottomInset) { let leftPx = anchorX - elementWidth / 2; let topPx = anchorY - elementHeight; const maxLeftPx = anchorBox.w - elementWidth; const maxTopPx = anchorBox.h - bottomInset - elementHeight; leftPx = maxLeftPx >= 0 ? clampToRange(leftPx, 0, maxLeftPx) : maxLeftPx / 2; topPx = maxTopPx >= 0 ? clampToRange(topPx, 0, maxTopPx) : 0; return { anchorX: leftPx + elementWidth / 2, anchorY: topPx + elementHeight }; } updateContainerPosition(subtitlesContainer, leftPct, topPct) { if (this.lastAppliedLeftPct === null || Math.abs(leftPct - this.lastAppliedLeftPct) >= .01) { subtitlesContainer.style.left = `${leftPct}%`; this.lastAppliedLeftPct = leftPct; } if (this.lastAppliedTopPct === null || Math.abs(topPct - this.lastAppliedTopPct) >= .01) { subtitlesContainer.style.top = `${topPct}%`; this.lastAppliedTopPct = topPct; } } resolvePresetAnchorPosition({ preset, anchorBox, elementWidth, elementHeight, bottomInset }) { let anchorX = anchorBox.w / 2; let anchorY = anchorBox.h - bottomInset; switch (preset) { case "top-center": anchorY = elementHeight; break; case "center": anchorY = anchorBox.h / 2 + elementHeight / 2; break; case "bottom-left": anchorX = elementWidth / 2; break; case "bottom-right": anchorX = anchorBox.w - elementWidth / 2; } return clampAnchorWithinBox({ anchorX, anchorY, elementWidth, elementHeight, boxWidth: anchorBox.w, boxHeight: anchorBox.h, bottomInset }); } applyPositionAfterContentRender() { const layout = this.getLayoutSize(); if (layout.w && layout.h) { const anchorBox = this.computeAnchorBoxLayout(layout); if (anchorBox.w && anchorBox.h) { this.refreshBottomInsetNow(layout, anchorBox); this.applySubtitlePositionWithLayout(layout, anchorBox); return; } this.refreshBottomInsetNow(layout); this.applySubtitlePosition(); return; } this.refreshBottomInsetNow(); this.applySubtitlePosition(); } trimEdgeWhitespaceTokens(tokens) { if (!tokens.length) return tokens; let s = 0; let e = tokens.length; while (s < e && !tokens[s]?.text.trim()) s += 1; while (e > s && !tokens[e - 1]?.text.trim()) e -= 1; if (s === 0 && e === tokens.length) return tokens; return s >= e ? [] : tokens.slice(s, e); } selectTokensByMaxLength(tokens, time) { if (!tokens.length) return tokens; let start = 0; let length = 0; let overflowed = false; let chosenStart = 0; let chosenEnd = tokens.length; let hasChosenRange = false; let matchedByTime = false; const considerRange = (rangeStart, rangeEnd) => { if (rangeEnd <= rangeStart) return; if (!hasChosenRange) { chosenStart = rangeStart; chosenEnd = rangeEnd; hasChosenRange = true; } if (matchedByTime) return; const first = tokens[rangeStart]; const last = tokens[rangeEnd - 1]; if (!first || !last) return; const endMs = (rangeEnd < tokens.length ? tokens[rangeEnd]?.startMs : void 0) ?? last.startMs + (last.durationMs ?? 0); if (first.startMs <= time && time < endMs) { chosenStart = rangeStart; chosenEnd = rangeEnd; matchedByTime = true; } }; for (const [index, token] of tokens.entries()) { const nextLength = length + token.text.length; if (nextLength > this.maxLength && index > start) { overflowed = true; considerRange(start, index); start = index; length = token.text.length; continue; } length = nextLength; } if (!overflowed) return this.trimEdgeWhitespaceTokens(tokens); considerRange(start, tokens.length); return this.trimEdgeWhitespaceTokens(tokens.slice(chosenStart, chosenEnd)); } buildTokenPrecomputeInput(tokens) { const cached = this.tokenPrecomputeMemo; if (cached?.tokens === tokens) return cached.value; const { slices, key } = buildWordSlices(tokens); const value = { wordSlices: slices, normalizedWordsKey: key }; this.tokenPrecomputeMemo = { tokens, value }; return value; } getTokenLayoutInputs(ctx) { const block = this.subtitlesBlock; if (block) { const cs = getComputedStyle(block); const fontKey = `${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`; ctx.font = fontKey; const cssMaxWidth = Number.parseFloat(cs.maxWidth); const paddingLeft = Number.parseFloat(cs.paddingLeft) || 0; const paddingRight = Number.parseFloat(cs.paddingRight) || 0; const baseMaxWidth = Number.isFinite(cssMaxWidth) ? cssMaxWidth : this.subtitleMaxWidthPx || globalThis.innerWidth * .8; if (Number.isFinite(baseMaxWidth) && baseMaxWidth > 0) this.subtitleMaxWidthPx = baseMaxWidth; return { fontKey, maxWidthPx: Math.max(0, baseMaxWidth - paddingLeft - paddingRight) }; } const remPx = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; const baseMaxWidth = Math.min(remPx * 52, this.subtitleMaxWidthPx || globalThis.innerWidth * .8); const fontSizePx = this.fontSizeOverridden ? this.fontSize : Math.min(24, Math.max(14, globalThis.innerWidth * .016)); const fontKey = `normal normal 500 ${fontSizePx}px ${getSubtitleFontFamilyCssValue(this.fontFamily)}`; ctx.font = fontKey; return { fontKey, maxWidthPx: Math.max(0, baseMaxWidth - fontSizePx) }; } getActiveLineKey(tokens) { if (this.lastActiveLineKey !== null) return this.lastActiveLineKey; return `${tokens[0]?.startMs ?? 0}:${tokens[0]?.durationMs ?? 0}:${tokens.length}`; } getLineMeasureMemo(tokens, activeLineKey) { const { wordSlices, normalizedWordsKey } = this.buildTokenPrecomputeInput(tokens); if (!wordSlices.length) return null; const ctx = this.getMeasureContext(); if (!ctx) return null; const { fontKey, maxWidthPx } = this.getTokenLayoutInputs(ctx); if (!Number.isFinite(maxWidthPx) || maxWidthPx < 24) return null; const key = `${activeLineKey}|${fontKey}|${Math.round(maxWidthPx)}|${normalizedWordsKey}`; if (this.lineMeasureMemo?.key === key) return this.lineMeasureMemo; const memo = { key, metrics: measureWordSlices(wordSlices, (text) => ctx.measureText(text).width), maxWidthPx }; this.lineMeasureMemo = memo; return memo; } buildTokenProcessingMemo(tokens, activeLineKey) { const lineMeasure = this.getLineMeasureMemo(tokens, activeLineKey); if (!lineMeasure) return null; const memoKey = `${lineMeasure.key}|${this.maxLength}`; if (this.tokenProcessingMemo?.key === memoKey) return this.tokenProcessingMemo; const safeMaxWidthPx = applyWrapWidthGuard(lineMeasure.maxWidthPx); const memo = { key: memoKey, segmentRanges: computeTwoLineSegments(tokens, lineMeasure.metrics, safeMaxWidthPx, this.maxLength) }; this.tokenProcessingMemo = memo; this.lastSegmentIndex = 0; return memo; } selectSegmentIndexFromRanges(segmentRanges, time) { if (!segmentRanges.length) return -1; let idx = this.lastSegmentIndex; if (idx >= segmentRanges.length) idx = 0; while (idx < segmentRanges.length - 1 && time >= segmentRanges[idx].endMs) idx += 1; while (idx > 0 && time < segmentRanges[idx].startMs) idx -= 1; if (!(time >= segmentRanges[idx].startMs && time < segmentRanges[idx].endMs)) { const found = segmentRanges.findIndex((s) => time >= s.startMs && time < s.endMs); if (found >= 0) idx = found; else idx = time < segmentRanges[0].startMs ? 0 : segmentRanges.length - 1; } this.lastSegmentIndex = idx; return idx; } processTokens(tokens, time) { if (!tokens.length) return tokens; const activeLineKey = this.getActiveLineKey(tokens); const memo = this.buildTokenProcessingMemo(tokens, activeLineKey); if (!memo) return this.selectTokensByMaxLength(tokens, time); const { segmentRanges } = memo; if (!segmentRanges.length) return this.trimEdgeWhitespaceTokens(tokens); const segmentIndex = this.selectSegmentIndexFromRanges(segmentRanges, time); if (segmentIndex < 0) return this.trimEdgeWhitespaceTokens(tokens); const seg = segmentRanges[segmentIndex]; return this.trimEdgeWhitespaceTokens(tokens.slice(seg.startToken, seg.endToken)); } async translateStrTokens(text) { const fromLang = this.subtitleLang ?? ""; const toLang = localizationProvider.lang; if (this.strTranslatedTokens) { const translated = await translate(text, fromLang, toLang); return [this.strTranslatedTokens, typeof translated === "string" ? translated : ""]; } const translated = await translate([this.strTokens, text], fromLang, toLang); const pair = Array.isArray(translated) ? translated : [translated, translated]; const context = typeof pair[0] === "string" ? pair[0] : ""; const current = typeof pair[1] === "string" ? pair[1] : ""; this.strTranslatedTokens = context; return [context, current]; } findTokenSpan(candidate, root) { let element = null; if (candidate instanceof Element) element = candidate; else if (candidate instanceof Text) element = candidate.parentElement; const token = element?.closest("span[data-vot-token=\"1\"]"); return token instanceof HTMLSpanElement && root.contains(token) ? token : null; } resolveTokenSpanFromClick(event) { const root = this.subtitlesBlock ?? this.subtitlesContainer; if (!root) return null; const fromTarget = this.findTokenSpan(event.target, root); if (fromTarget) return fromTarget; const path = typeof event.composedPath === "function" ? event.composedPath() : []; for (const node of path) { const fromPath = this.findTokenSpan(node, root); if (fromPath) return fromPath; } const x = event.clientX; const y = event.clientY; return Number.isFinite(x) && Number.isFinite(y) ? this.findTokenSpan(document.elementFromPoint(x, y), root) : null; } releaseTooltip() { this.tooltipTranslationRequestId += 1; if (this.tokenTooltip?.target) this.tokenTooltip.target.classList.remove("selected"); this.tokenTooltip?.release(); this.tokenTooltip = void 0; destroyShadowMount(this.tooltipMount); this.tooltipMount = void 0; return this; } clearPendingSchedulerState() { this.repositionPending = false; this.updatePending = false; this.wrapPending = false; this.positionRefreshPending = false; } clearRenderedContent({ releaseTooltip = false } = {}) { if (releaseTooltip) this.releaseTooltip(); this.resetRenderMemo(); this.lastActiveLineKey = null; this.strTokens = ""; this.resetTranslationContext(); this.subtitlesBlock = null; this.renderedHighlightEls = []; this.resetWrapMemo(); this.lastWrapTokens = null; this.subtitleMaxWidthPx = 0; this.smartAnchorWidthPx = 0; this.smartAnchorHeightPx = 0; this.smartFontSizePx = 0; this.smartMaxWidthPx = 0; this.lastAppliedLeftPct = null; this.lastAppliedTopPct = null; this.passedStateKey = null; this.passedThresholds.length = 0; this.insetCacheReady = false; this.hideSnapGuides(); this.resetSegmentationMemo(); this.clearPendingSchedulerState(); if (this.subtitlesContainer) D(null, this.subtitlesContainer); } onClick = async (event) => { if (performance.now() < this.suppressTokenClicksUntil) { event.preventDefault(); event.stopPropagation(); return; } const target = this.resolveTokenSpanFromClick(event); if (!target) { this.releaseTooltip(); return; } if (this.toggleCurrentTooltipTarget(target)) return; this.releaseTooltip(); const requestId = this.tooltipTranslationRequestId; const text = this.normalizeTokenTextForTranslation(target.textContent ?? ""); if (!text) return; const service = await votStorage.get("translationService", defaultTranslationService); if (requestId !== this.tooltipTranslationRequestId) return; target.classList.add("selected"); const subtitlesInfo = UI.createSubtitleInfo(text, this.strTranslatedTokens || this.strTokens, service); const tooltip = this.createTokenTooltip(target, subtitlesInfo.container); this.tokenTooltip = tooltip; tooltip.create(); const strTokens = this.strTokens; const translated = await this.translateStrTokens(text); if (requestId !== this.tooltipTranslationRequestId) return; if (this.shouldSkipTooltipUpdate(requestId, tooltip, target, strTokens)) return; subtitlesInfo.header.textContent = translated[1]; subtitlesInfo.context.textContent = translated[0]; tooltip.setContent(subtitlesInfo.container); }; toggleCurrentTooltipTarget(target) { if (this.tokenTooltip?.target !== target || !this.tokenTooltip?.container) return false; if (this.tokenTooltip.showed) target.classList.add("selected"); else target.classList.remove("selected"); return true; } createTokenTooltip(target, content) { const viewportWidth = Math.max(320, globalThis.innerWidth || 0); const preferredWidth = Math.max(360, this.subtitlesContainer?.offsetWidth ?? 0, this.subtitlesBlock?.offsetWidth ?? 0, Math.min(this.subtitleMaxWidthPx || 0, 720)); const tooltipMaxWidth = Math.min(viewportWidth - 24, preferredWidth, 720); const tooltipMount = this.ensureTooltipMount(); return new Tooltip({ target, anchor: this.subtitlesBlock ?? target, content, parentElement: tooltipMount.root, offset: { x: 4, y: 12 }, maxWidth: tooltipMaxWidth, mode: "follow", borderRadius: 12, bordered: false, position: "top", trigger: "click" }); } shouldSkipTooltipUpdate(requestId, tooltip, target, strTokens) { return requestId !== this.tooltipTranslationRequestId || strTokens !== this.strTokens || this.tokenTooltip !== tooltip || tooltip.target !== target || !tooltip.showed; } buildPassedState(tokens, time, stateKey) { if (this.passedStateKey !== stateKey) { this.passedStateKey = stateKey; this.passedThresholds.length = 0; for (const token of tokens) { if (!token.isWordLike) continue; const halfway = token.startMs + token.durationMs / 2; const earlyPassThreshold = Math.max(token.startMs - 100, halfway - 275); this.passedThresholds.push(Math.min(halfway, earlyPassThreshold)); } } const flags = this.passedFlagsBuffer; const thresholds = this.passedThresholds; for (let i = 0; i < thresholds.length; i += 1) flags[i] = time > thresholds[i]; flags.length = thresholds.length; return flags; } renderTokens(tokens) { return buildSubtitleRenderPlan(tokens, tokens.length - 1, this.breakAfterTokenIndexSet).map((part) => this.renderPlanPart(part)); } renderStyledSpan(text, style, isWordToken = false, highlightIndex) { if (!style && !isWordToken && highlightIndex === void 0) return text; return b`${text}`; } renderPlanPart(part) { if (part.kind === "break") return b`
`; return this.renderStyledSpan(part.text, part.style, part.kind === "word", part.highlightIndex); } updatePassedClasses(passedFlags) { for (const tokenEl of this.renderedHighlightEls) { const highlightIndex = Number.parseInt(tokenEl.dataset.votHighlightIndex ?? "", 10); const isPassed = Number.isInteger(highlightIndex) && highlightIndex >= 0 && highlightIndex < passedFlags.length ? passedFlags[highlightIndex] : false; tokenEl.classList.toggle("passed", isPassed); } } clearPassedClasses() { for (const tokenEl of this.renderedHighlightEls) tokenEl.classList.remove("passed"); } setBreakAfterTokenIndices(indices) { this.breakAfterTokenIndices = indices; this.breakAfterTokenIndexSet = indices.length ? new Set(indices) : null; } scheduleWrapRecompute(tokens = null) { if (tokens) this.lastWrapTokens = tokens; const shouldRequestTick = !this.wrapPending; this.wrapPending = true; if (shouldRequestTick) this.intervalIdleChecker.requestImmediateTick(); } maybeRefreshPosition(force = false) { if (this.abortController.signal.aborted) return; if (!this.subtitlesContainer) return; const now = performance.now(); if (!force && now - this.lastPositionRefreshTs < this.positionRefreshIntervalMs) return; this.lastPositionRefreshTs = now; this.positionRefreshPending = true; this.intervalIdleChecker.requestImmediateTick(); } getMeasureContext(font) { if (!this.measureCanvas) { this.measureCanvas = document.createElement("canvas"); this.measureCanvas.width = 1; this.measureCanvas.height = 1; } if (!this.measureCtx) this.measureCtx = this.measureCanvas.getContext("2d", { alpha: false }) ?? this.measureCanvas.getContext("2d"); if (!this.measureCtx) return null; if (typeof font === "string" && font) this.measureCtx.font = font; return this.measureCtx; } recomputeWrapNow() { const tokens = this.lastWrapTokens; const block = this.subtitlesBlock; if (!tokens || !block) return; const ctx = this.getMeasureContext(); if (!ctx) return; const { fontKey, maxWidthPx } = this.getTokenLayoutInputs(ctx); if (!Number.isFinite(maxWidthPx) || maxWidthPx < 50) return; const safeMaxWidthPx = applyWrapWidthGuard(maxWidthPx); if (safeMaxWidthPx < 50) return; const wrapKey = `${this.getActiveLineKey(tokens)}|${fontKey}|${Math.round(safeMaxWidthPx)}|${this.stringifyTokens(tokens)}`; if (wrapKey === this.lastWrapKey) return; this.lastWrapKey = wrapKey; const next = computeTokenWrapPlan(tokens, (text) => ctx.measureText(text).width, safeMaxWidthPx); if (next.breakAfterTokenIndices.length !== this.breakAfterTokenIndices.length || next.breakAfterTokenIndices.some((value, index) => value !== this.breakAfterTokenIndices[index])) { this.setBreakAfterTokenIndices(next.breakAfterTokenIndices); this.resetRenderMemo(); this.update(); } } setContent(subtitles, lang = void 0) { this.releaseTooltip(); this.subtitleLang = lang; if (!subtitles || !this.video) { this.clearRenderedContent(); this.subtitles = null; this.maxActiveCueLookbackMs = 0; this.lastPlaybackTimeMs = null; this.clearPendingSchedulerState(); this.stopVideoFrameLoop(); this.detachDragDocumentListeners(); return; } this.createSubtitlesContainer(); this.subtitles = subtitles; this.maxActiveCueLookbackMs = subtitles.subtitles.reduce((maxDurationMs, line) => Math.max(maxDurationMs, Math.max(0, line.durationMs)), 0); this.lastPlaybackTimeMs = Math.max(0, this.video.currentTime * 1e3); this.lastActiveLineKey = null; this.syncVideoFrameLoop(); this.updateContainerRect(); this.update(); this.intervalIdleChecker.requestImmediateTick(); } setMaxLength(len) { if (typeof len === "number" && len > 0) { this.maxLength = len; this.resetSegmentationMemo(); this.update(); this.scheduleReposition(); } } setHighlightWords(value) { const wasEnabled = this.highlightWords; this.highlightWords = Boolean(value); if (wasEnabled && !this.highlightWords) this.clearPassedClasses(); this.update(); } setSmartLayout(enabled) { const next = enabled !== false; if (next === this.smartLayoutEnabled) return; this.smartLayoutEnabled = next; this.subtitlesContainer?.style.removeProperty("--vot-subtitles-max-width"); this.lastSmartLayoutKey = null; this.resetWrapMemo(); this.resetRenderMemo(); this.resetSegmentationMemo(); this.applyManualFontSizeStyle(); this.update(); this.scheduleWrapRecompute(); this.scheduleReposition(); } setFontSize(size) { this.fontSize = size; this.fontSizeOverridden = true; if (!this.smartLayoutEnabled) { this.applyManualFontSizeStyle(); this.lastWrapKey = null; this.resetSegmentationMemo(); this.scheduleWrapRecompute(); this.scheduleReposition(); } } setFontFamily(fontFamily) { this.fontFamily = fontFamily; this.applyFontFamilyStyle(); this.lastWrapKey = null; this.resetSegmentationMemo(); this.scheduleWrapRecompute(); this.scheduleReposition(); } setOpacity(rate) { const numericRate = Number(rate); const clampedRate = Number.isFinite(numericRate) ? clampToRange(numericRate, 0, 100) : 0; this.opacity = ((100 - clampedRate) / 100).toFixed(2); this.applyOpacityStyle(); } stringifyTokens(tokens) { return tokens.map((token) => token.text).join(""); } resolveActiveLine(time, subtitlesList) { return buildActiveSubtitleRenderLine(time, subtitlesList, this.maxActiveCueLookbackMs); } clearInactiveLineState() { this.lastActiveLineKey = null; if (this.subtitlesBlock || this.lastRenderKey !== null || this.strTokens) { this.clearRenderedContent({ releaseTooltip: true }); return; } this.releaseTooltip(); } refreshSmartLayoutIfNeeded() { if (!this.smartLayoutEnabled) return; const now = performance.now(); if (this.lastSmartLayoutKey !== null && now - this.lastSmartLayoutCheckTs <= 500) return; this.lastSmartLayoutCheckTs = now; const layout = this.getLayoutSize(); if (!layout.w || !layout.h) return; const anchorBox = this.computeAnchorBoxLayout(layout); if (anchorBox.w && anchorBox.h) this.ensureSmartLayout(anchorBox); } getRenderState(line, activeLineKey, time) { const tokens = this.processTokens(line.tokens, time); this.lastWrapTokens = tokens; const strTokens = this.stringifyTokens(tokens); const tokensChanged = strTokens !== this.strTokens; if (tokensChanged) { this.releaseTooltip(); this.strTokens = strTokens; this.resetTranslationContext(); this.resetWrapMemo(); } const passedStateKey = `${activeLineKey}:${strTokens}`; return { tokens, tokensChanged, passedFlags: this.highlightWords ? this.buildPassedState(tokens, time, passedStateKey) : null, renderKey: `${activeLineKey}:${strTokens}:${this.breakAfterTokenIndices.join(",")}` }; } syncRenderedTokens(tokens) { this.subtitlesContainer = this.subtitlesContainer ?? this.createSubtitlesContainer(); D(b` ${this.renderTokens(tokens)} `, this.subtitlesContainer); const firstChild = this.subtitlesContainer.firstElementChild; this.subtitlesBlock = firstChild instanceof HTMLElement && firstChild.classList.contains("vot-subtitles") ? firstChild : null; this.renderedHighlightEls = this.subtitlesBlock ? Array.from(this.subtitlesBlock.querySelectorAll("span[data-vot-highlight-index]")) : []; } update() { if (!this.video || !this.subtitles) return; const time = this.resolvePlaybackTimeMs(); const subtitlesList = this.subtitles.subtitles; const activeLine = this.resolveActiveLine(time, subtitlesList); if (!activeLine) { this.clearInactiveLineState(); return; } this.lastActiveLineKey = activeLine.lineKey; this.refreshSmartLayoutIfNeeded(); const { tokens, tokensChanged, passedFlags, renderKey } = this.getRenderState(activeLine.line, activeLine.lineKey, time); if (renderKey === this.lastRenderKey) { if (this.highlightWords && !tokensChanged && passedFlags) this.updatePassedClasses(passedFlags); this.maybeRefreshPosition(); return; } this.lastRenderKey = renderKey; this.syncRenderedTokens(tokens); if (this.highlightWords && passedFlags) this.updatePassedClasses(passedFlags); if (tokensChanged) { this.applyPositionAfterContentRender(); this.scheduleWrapRecompute(tokens); this.scheduleReposition(); } else this.maybeRefreshPosition(); } release() { this.detachDragDocumentListeners(); this.stopVideoFrameLoop(); this.abortController.abort(); this.resizeObserver?.disconnect(); this.clearPendingSchedulerState(); this.checkerUnsubscribe?.(); this.checkerUnsubscribe = null; this.releaseTooltip(); if (this.subtitlesContainer) { this.subtitlesContainer.remove(); this.subtitlesContainer = null; } destroyShadowMount(this.tooltipMount); this.tooltipMount = void 0; this.fullscreenLayerController.release(); if (this.safeAreaProbeEl) { this.safeAreaProbeEl.remove(); this.safeAreaProbeEl = null; } if (this.guidesLayer) { this.guidesLayer.remove(); this.guidesLayer = null; this.verticalGuide = null; this.horizontalGuide = null; } this.measureCtx = null; this.measureCanvas = null; this.lastAppliedLeftPct = null; this.lastAppliedTopPct = null; this.passedStateKey = null; this.passedThresholds.length = 0; this.insetCacheReady = false; } }; //#endregion //#region src/utils/download.ts function toUint32BE(value) { return new Uint8Array([ value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255 ]); } function toSynchsafeInt(value) { return new Uint8Array([ value >>> 21 & 127, value >>> 14 & 127, value >>> 7 & 127, value & 127 ]); } function addTitleId3Tag(mp3Buffer, title) { const titleBytes = new TextEncoder().encode(title); const frameData = new Uint8Array(titleBytes.length + 1); frameData[0] = 3; frameData.set(titleBytes, 1); const frame = new Uint8Array(10 + frameData.length); frame.set([ 84, 73, 84, 50 ], 0); frame.set(toUint32BE(frameData.length), 4); frame.set(frameData, 10); const header = /* @__PURE__ */ new Uint8Array(10); header.set([ 73, 68, 51, 3, 0, 0 ], 0); header.set(toSynchsafeInt(frame.length), 6); const audioBytes = new Uint8Array(mp3Buffer); const out = new Uint8Array(header.length + frame.length + audioBytes.length); out.set(header, 0); out.set(frame, header.length); out.set(audioBytes, header.length + frame.length); return new Blob([out], { type: "audio/mpeg" }); } function appendChunkToOutputBuffer(out, value, loaded) { const needed = loaded + value.byteLength; let nextOut = out; if (needed > nextOut.length) { const grown = new Uint8Array(Math.max(needed, nextOut.length * 2)); grown.set(nextOut.subarray(0, loaded)); nextOut = grown; } nextOut.set(value, loaded); return { out: nextOut, loaded: needed }; } function mergeChunks(chunks, loaded) { const merged = new Uint8Array(loaded); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; } return merged.buffer; } async function readResponseArrayBuffer(res, onProgress) { const total = Number(res.headers.get("Content-Length") ?? 0); if (!res.body) return res.arrayBuffer(); const reader = res.body.getReader(); let loaded = 0; let out = total > 0 ? new Uint8Array(total) : null; const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; if (!value || value.byteLength === 0) continue; if (out) { const appended = appendChunkToOutputBuffer(out, value, loaded); out = appended.out; loaded = appended.loaded; } else { chunks.push(value); loaded += value.byteLength; } if (total > 0) onProgress(clamp(Math.round(loaded / total * 100))); } if (out) return out.buffer.slice(0, loaded); return mergeChunks(chunks, loaded); } /** * Downloads a translation file and saves it as an MP3 file with metadata, * tracking progress when Content-Length is available. */ async function downloadTranslation(res, filename, onProgress = () => {}, saveOptions = {}) { return await downloadBlob(await buildTranslationBlob(res, filename, onProgress), `${filename}.mp3`, saveOptions); } async function buildTranslationBlob(res, filename, onProgress = () => {}) { const arrayBuffer = await readResponseArrayBuffer(res, onProgress); onProgress(100); return addTitleId3Tag(arrayBuffer, filename); } //#endregion //#region src/subtitles/displayModel.ts var LEADING_SPEAKER_MARKER_RE = /^(\s*)>>\s*/u; var ATTACHED_TIME_WORD_RE = /(\d{1,2}:\d{2}(?::\d{2})?)(?=[\p{L}\p{M}])/gu; var LETTER_OR_MARK_RUN_RE = /[\p{L}\p{M}]+/uy; var DIGIT_RUN_RE = /\d+/y; var ASS_DIRECTIVE_RE = /\\[^\\]+/gu; var ASS_STYLE_TOGGLE_RE = /^\\([ibu])([01])$/u; var ASS_PRIMARY_COLOR_RE = /^\\(?:1?c|c)&H([0-9a-f]{6,8})&$/iu; var ASS_STYLE_RESET_RE = /^\\r[^\\}]*$/u; var COMPLEX_DISPLAY_CONTROL_RE = /[<{\\]/u; var cloneMutableInlineStyle = (style) => ({ italic: style.italic, bold: style.bold, underline: style.underline, color: style.color, classes: [...style.classes] }); var assignMutableInlineStyle = (target, source) => { target.italic = source.italic; target.bold = source.bold; target.underline = source.underline; target.color = source.color; target.classes = [...source.classes]; }; var resetMutableInlineStyle = (style) => { style.italic = false; style.bold = false; style.underline = false; style.color = void 0; style.classes = []; }; var toTokenStyle = (style) => normalizeSubtitleInlineStyle({ italic: style.italic, bold: style.bold, underline: style.underline, color: style.color, classes: style.classes }); var pushSegment = (segments, text, style) => { if (!text) return; const tokenStyle = toTokenStyle(style); const previous = segments.at(-1); if (previous && subtitleInlineStylesEqual(previous.style, tokenStyle)) { previous.text += text; return; } segments.push({ text, style: tokenStyle }); }; var isHtmlTagSpace = (char) => char === " " || char === " "; var isAsciiAlphaNumeric = (char) => { const code = char.charCodeAt(0); return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122; }; var parseHtmlTagToken = (text) => { if (!text.startsWith("<")) return null; let index = 1; while (isHtmlTagSpace(text[index] ?? "")) index += 1; const isClosing = text[index] === "/"; if (isClosing) { index += 1; while (isHtmlTagSpace(text[index] ?? "")) index += 1; } const tagNameStart = index; while (isAsciiAlphaNumeric(text[index] ?? "")) index += 1; if (index === tagNameStart) return null; const tagEndIndex = text.indexOf(">", index); if (tagEndIndex < 0) return null; return { tagName: text.slice(tagNameStart, index).toLowerCase(), attrsRaw: text.slice(index, tagEndIndex), isClosing, length: tagEndIndex + 1 }; }; var parseAssColorToCssHex = (value) => { const normalized = value.trim(); if (!/^[0-9a-f]{6,8}$/iu.test(normalized)) return; const bgr = normalized.slice(-6); const blue = bgr.slice(0, 2); const green = bgr.slice(2, 4); return normalizeCssColorValue(`#${bgr.slice(4, 6)}${green}${blue}`); }; var applyAssStyleDirective = (directive, style) => { const toggleMatch = ASS_STYLE_TOGGLE_RE.exec(directive.trim()); if (toggleMatch) { const enabled = toggleMatch[2] === "1"; if (toggleMatch[1] === "i") { style.italic = enabled; return; } if (toggleMatch[1] === "b") { style.bold = enabled; return; } if (toggleMatch[1] === "u") { style.underline = enabled; return; } } if (ASS_STYLE_RESET_RE.test(directive.trim())) { resetMutableInlineStyle(style); return; } const colorMatch = ASS_PRIMARY_COLOR_RE.exec(directive.trim()); if (colorMatch) style.color = parseAssColorToCssHex(colorMatch[1]); }; var applyAssOverrideBlock = (rawDirectives, style) => { const directives = rawDirectives.match(ASS_DIRECTIVE_RE) ?? []; for (const directive of directives) applyAssStyleDirective(directive, style); }; var normalizeLeadingSpeakerMarker = (segments) => { for (const segment of segments) { if (!segment.text) continue; const normalized = segment.text.replace(LEADING_SPEAKER_MARKER_RE, "$1"); segment.text = normalized; if (normalized.length > 0) break; } while (segments[0]?.text === "") segments.shift(); }; var normalizeAttachedTimeExpressions = (segments) => { for (const segment of segments) { if (!segment.text) continue; segment.text = segment.text.replaceAll(ATTACHED_TIME_WORD_RE, "$1 "); } }; var normalizeAttachedWordNumberExpressions = (segments) => { for (const segment of segments) { if (!segment.text) continue; segment.text = normalizeAttachedWordNumberExpression(segment.text); } }; var isAsciiCodeLike = (letters) => /^[A-Za-z]{1,3}$/u.test(letters) || letters.length === 1 && letters === letters.toLocaleUpperCase() && letters !== letters.toLocaleLowerCase(); var readRunEndIndex = (text, startIndex, regex) => { regex.lastIndex = startIndex; return regex.exec(text) ? regex.lastIndex : startIndex; }; var getNextCodePointIndex = (text, startIndex) => { return startIndex + ((text.codePointAt(startIndex) ?? 0) > 65535 ? 2 : 1); }; var formatAttachedWordNumberMatch = (match, letters, digits, lettersFirst) => { if (isAsciiCodeLike(letters)) return match; return lettersFirst ? `${letters} ${digits}` : `${digits} ${letters}`; }; var normalizeAttachedWordNumberExpression = (text) => { const parts = []; for (let index = 0; index < text.length;) { const lettersEnd = readRunEndIndex(text, index, LETTER_OR_MARK_RUN_RE); if (lettersEnd > index) { const digitsEnd = readRunEndIndex(text, lettersEnd, DIGIT_RUN_RE); if (digitsEnd > lettersEnd) { const letters = text.slice(index, lettersEnd); const digits = text.slice(lettersEnd, digitsEnd); const match = text.slice(index, digitsEnd); parts.push(formatAttachedWordNumberMatch(match, letters, digits, true)); index = digitsEnd; continue; } parts.push(text.slice(index, lettersEnd)); index = lettersEnd; continue; } else { const digitsEnd = readRunEndIndex(text, index, DIGIT_RUN_RE); if (digitsEnd > index) { const lettersEndAfterDigits = readRunEndIndex(text, digitsEnd, LETTER_OR_MARK_RUN_RE); if (lettersEndAfterDigits > digitsEnd) { const digits = text.slice(index, digitsEnd); const letters = text.slice(digitsEnd, lettersEndAfterDigits); const match = text.slice(index, lettersEndAfterDigits); parts.push(formatAttachedWordNumberMatch(match, letters, digits, false)); index = lettersEndAfterDigits; continue; } parts.push(text.slice(index, digitsEnd)); index = digitsEnd; continue; } } const nextIndex = getNextCodePointIndex(text, index); parts.push(text.slice(index, nextIndex)); index = nextIndex; } return parts.join(""); }; var extractHtmlTagClasses = (attrsRaw) => { const normalized = attrsRaw.trim(); if (!normalized.startsWith(".")) return; const classNames = normalized.split(/\s+/u, 1)[0].split(".").filter(Boolean); return classNames.length ? classNames : void 0; }; var extractHtmlFontColor = (attrsRaw) => { const match = /\bcolor\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/iu.exec(attrsRaw); const rawColor = match?.[1] ?? match?.[2] ?? match?.[3]; return rawColor ? normalizeCssColorValue(rawColor) : void 0; }; var popHtmlStyleFrame = (tagName, stack, activeStyle) => { for (let i = stack.length - 1; i >= 0; i -= 1) { if (stack[i].tagName !== tagName) continue; const [frame] = stack.splice(i, 1); if (!frame) return; assignMutableInlineStyle(activeStyle, frame.previousStyle); return; } if (tagName === "b") { activeStyle.bold = false; return; } if (tagName === "i") { activeStyle.italic = false; return; } if (tagName === "u") { activeStyle.underline = false; return; } if (tagName === "font") { activeStyle.color = void 0; return; } if (tagName === "c") activeStyle.classes = []; }; var applyHtmlTagStyle = (htmlTag, segments, activeStyle, styleStack) => { const { attrsRaw, isClosing, tagName } = htmlTag; if (tagName === "br") { pushSegment(segments, "\n", activeStyle); return; } if (isClosing) { popHtmlStyleFrame(tagName, styleStack, activeStyle); return; } if (![ "b", "i", "u", "font", "c" ].includes(tagName)) return; styleStack.push({ tagName, previousStyle: cloneMutableInlineStyle(activeStyle) }); if (tagName === "b") { activeStyle.bold = true; return; } if (tagName === "i") { activeStyle.italic = true; return; } if (tagName === "u") { activeStyle.underline = true; return; } if (tagName === "font") { const color = extractHtmlFontColor(attrsRaw); if (color) activeStyle.color = color; return; } activeStyle.classes = extractHtmlTagClasses(attrsRaw) ?? []; }; var consumeDisplayControlToken = (rawText, cursor, segments, activeStyle, styleStack) => { const remainder = rawText.slice(cursor); if (remainder.startsWith(String.raw`\N`) || remainder.startsWith(String.raw`\n`)) { pushSegment(segments, "\n", activeStyle); return cursor + 2; } if (remainder.startsWith(String.raw`\h`)) { pushSegment(segments, " ", activeStyle); return cursor + 2; } if (remainder.startsWith("\n")) { pushSegment(segments, "\n", activeStyle); return cursor + 1; } if (remainder.startsWith("{")) { const assOverrideEndIndex = remainder.indexOf("}", 1); if (assOverrideEndIndex > 0) { applyAssOverrideBlock(remainder.slice(1, assOverrideEndIndex), activeStyle); return cursor + assOverrideEndIndex + 1; } } const htmlTag = parseHtmlTagToken(remainder); if (!htmlTag) return null; applyHtmlTagStyle(htmlTag, segments, activeStyle, styleStack); return cursor + htmlTag.length; }; var buildStyledSpans = (segments) => { const styledSpans = []; let text = ""; for (const segment of segments) { if (!segment.text) continue; const start = text.length; text += segment.text; styledSpans.push({ start, end: text.length, style: segment.style }); } return { text, styledSpans }; }; var trimStyledDisplayResult = (text, styledSpans) => { const normalizedText = text.replaceAll("\xA0", " "); const leadingTrim = normalizedText.length - normalizedText.trimStart().length; const trailingTrim = normalizedText.length - normalizedText.trimEnd().length; const trimmedEnd = Math.max(leadingTrim, normalizedText.length - trailingTrim); const finalText = normalizedText.slice(leadingTrim, trimmedEnd); return { text: finalText, styledSpans: styledSpans.map((span) => ({ start: Math.max(0, span.start - leadingTrim), end: Math.max(0, span.end - leadingTrim), style: span.style })).filter((span) => span.end > span.start && span.start < finalText.length).map((span) => ({ ...span, end: Math.min(span.end, finalText.length) })) }; }; var trimPlainDisplayText = (text) => { const normalizedText = text.replaceAll("\xA0", " "); const leadingTrim = normalizedText.length - normalizedText.trimStart().length; const trailingTrim = normalizedText.length - normalizedText.trimEnd().length; const trimmedEnd = Math.max(leadingTrim, normalizedText.length - trailingTrim); return normalizedText.slice(leadingTrim, trimmedEnd); }; var buildPlainDisplayModel = (rawText) => ({ text: trimPlainDisplayText(normalizeAttachedWordNumberExpression(rawText.replace(LEADING_SPEAKER_MARKER_RE, "$1").replaceAll(ATTACHED_TIME_WORD_RE, "$1 "))), styledSpans: [] }); var buildStyledDisplayModel = (rawText) => { if (!rawText) return { text: "", styledSpans: [] }; if (!COMPLEX_DISPLAY_CONTROL_RE.test(rawText)) return buildPlainDisplayModel(rawText); const segments = []; const activeStyle = { italic: false, bold: false, underline: false, color: void 0, classes: [] }; const styleStack = []; let cursor = 0; while (cursor < rawText.length) { const nextCursor = consumeDisplayControlToken(rawText, cursor, segments, activeStyle, styleStack); if (nextCursor !== null) { cursor = nextCursor; continue; } pushSegment(segments, rawText[cursor], activeStyle); cursor += 1; } normalizeLeadingSpeakerMarker(segments); normalizeAttachedTimeExpressions(segments); normalizeAttachedWordNumberExpressions(segments); const built = buildStyledSpans(segments); return trimStyledDisplayResult(built.text, built.styledSpans); }; var getStyleForRange = (styledSpans, start, end) => { if (!styledSpans?.length || end <= start) return; return styledSpans.find((span) => start < span.end && end > span.start && span.style)?.style; }; //#endregion //#region src/subtitles/standards.ts var BOM = ""; var SRT_TIMING_RE = /^[ \t]*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})[ \t]*-->[ \t]*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})[ \t]*$/u; var VTT_TIMESTAMP_RE = /^(?:\d{2}:)?\d{2}:\d{2}\.\d{3}$/u; var isSpaceOrTab = (char) => char === " " || char === " "; var indexOfSpaceOrTab = (value) => { const spaceIndex = value.indexOf(" "); const tabIndex = value.indexOf(" "); if (spaceIndex === -1) return tabIndex; if (tabIndex === -1) return spaceIndex; return Math.min(spaceIndex, tabIndex); }; var trimStartSpaceOrTab = (value) => { let startIndex = 0; while (startIndex < value.length && isSpaceOrTab(value[startIndex])) startIndex += 1; return value.slice(startIndex); }; var parseVttTimingParts = (line) => { const arrowIndex = line.indexOf("-->"); if (arrowIndex <= 0) return null; const beforeArrow = line.slice(0, arrowIndex); if (!isSpaceOrTab(beforeArrow.at(-1) ?? "")) return null; const start = beforeArrow.trimEnd(); const afterArrow = line.slice(arrowIndex + 3); if (!isSpaceOrTab(afterArrow[0] ?? "")) return null; const endAndSettings = trimStartSpaceOrTab(afterArrow); const settingsSeparatorIndex = indexOfSpaceOrTab(endAndSettings); const end = settingsSeparatorIndex === -1 ? endAndSettings : endAndSettings.slice(0, settingsSeparatorIndex); const settings = settingsSeparatorIndex === -1 ? "" : trimStartSpaceOrTab(endAndSettings.slice(settingsSeparatorIndex)); if (!VTT_TIMESTAMP_RE.test(start) || !VTT_TIMESTAMP_RE.test(end) || settingsSeparatorIndex !== -1 && (!settings || /^\s/u.test(settings))) return null; return { start, end, settings }; }; var isVttTimingLine = (line) => parseVttTimingParts(line) != null; var extractAssOverrideTags = (rawText) => { const overrideTags = []; let cursor = 0; while (cursor < rawText.length) { const startIndex = rawText.indexOf("{", cursor); if (startIndex === -1) break; const endIndex = rawText.indexOf("}", startIndex + 1); if (endIndex === -1) break; overrideTags.push(rawText.slice(startIndex, endIndex + 1)); cursor = endIndex + 1; } return overrideTags; }; var normalizeNewlines = (value) => value.replace(BOM, "").replaceAll(/\r\n?/gu, "\n"); var padMilliseconds = (value) => value.length >= 3 ? value.slice(0, 3) : value.padEnd(3, "0"); var parseClockTime = (value, millisecondsLength) => { const parts = value.trim().split(":"); if (parts.length < 2 || parts.length > 3) return null; const [hoursRaw, minutesRaw, secondsRaw] = parts.length === 2 ? ["00", ...parts] : parts; const [secondsPart, millisecondsPart = "0"] = secondsRaw.split(/[.,]/u); if (!/^\d+$/u.test(hoursRaw) || !/^\d{2}$/u.test(minutesRaw) || !/^\d{2}$/u.test(secondsPart) || !/^\d+$/u.test(millisecondsPart)) return null; const hours = Number(hoursRaw); const minutes = Number(minutesRaw); const seconds = Number(secondsPart); const milliseconds = Number(padMilliseconds(millisecondsPart).slice(0, millisecondsLength)); if (!Number.isFinite(hours) || !Number.isFinite(minutes) || !Number.isFinite(seconds) || minutes > 59 || seconds > 59) return null; return ((hours * 60 + minutes) * 60 + seconds) * 1e3 + milliseconds; }; var formatClockTime = (totalMs, { delimiter, allowOptionalHours, fractionDigits }) => { const { hours, minutes, seconds, milliseconds } = splitTimestampParts(totalMs, Math.round); const fraction = milliseconds.toString().padStart(3, "0").slice(0, fractionDigits); return `${allowOptionalHours && hours === 0 ? "" : `${hours.toString().padStart(2, "0")}:`}${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}${delimiter}${fraction}`; }; var splitTimestampParts = (totalMs, normalizeMs) => { const safeMs = Math.max(0, normalizeMs(totalMs)); return { hours: Math.floor(safeMs / 36e5), minutes: Math.floor(safeMs % 36e5 / 6e4), seconds: Math.floor(safeMs % 6e4 / 1e3), milliseconds: safeMs % 1e3 }; }; var formatAssTime = (totalMs) => { const { hours, minutes, seconds, milliseconds } = splitTimestampParts(totalMs, Math.round); const centiseconds = Math.floor(milliseconds / 10); return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${centiseconds.toString().padStart(2, "0")}`; }; var parseAssTime = (value) => { const match = /^(?\d+):(?\d{2}):(?\d{2})\.(?\d{2})$/u.exec(value.trim()); if (!match?.groups) return null; const hours = Number(match.groups.hours); const minutes = Number(match.groups.minutes); const seconds = Number(match.groups.seconds); const centiseconds = Number(match.groups.centiseconds); if (minutes > 59 || seconds > 59 || !Number.isFinite(hours) || !Number.isFinite(centiseconds)) return null; return ((hours * 60 + minutes) * 60 + seconds) * 1e3 + centiseconds * 10; }; var normalizeSubtitleTextForDisplay = (value) => buildStyledDisplayModel(value).text; var getSubtitleLineEndMs = (line) => line.startMs + Math.max(0, line.durationMs); var getCueDurationMs = (startMs, endMs) => Math.max(0, endMs - startMs); var toComparableSubtitleOrder = (subtitles) => Array.from(subtitles).sort((left, right) => { const startDiff = left.line.startMs - right.line.startMs; if (startDiff !== 0) return startDiff; const endDiff = getSubtitleLineEndMs(left.line) - getSubtitleLineEndMs(right.line); if (endDiff !== 0) return endDiff; return left.index - right.index; }).map(({ line }) => line); var trimEmptyBoundaryLines = (lines) => { let start = 0; let end = lines.length; while (start < end && lines[start] === "") start += 1; while (end > start && lines[end - 1] === "") end -= 1; return lines.slice(start, end); }; var parseCueSettings = (rawSettings) => { const raw = rawSettings.trim(); if (!raw) return void 0; const values = {}; for (const token of raw.split(/\s+/u)) { const separatorIndex = token.indexOf(":"); if (separatorIndex <= 0) continue; values[token.slice(0, separatorIndex)] = token.slice(separatorIndex + 1); } return { raw, values }; }; var extractVttVoice = (payload) => { return /^[ \t]*]*))?>/iu.exec(payload)?.[1]?.trim() || void 0; }; var isSrtCueStart = (lines, index) => { const current = lines[index]?.trim() ?? ""; const next = lines[index + 1]?.trim() ?? ""; return SRT_TIMING_RE.test(current) || /^\d+$/u.test(current) && SRT_TIMING_RE.test(next); }; var getSrtTimingLineIndex = (lines, cursor) => /^\d+$/u.test(lines[cursor]?.trim() ?? "") && SRT_TIMING_RE.test(lines[cursor + 1]?.trim() ?? "") ? cursor + 1 : cursor; var parseSrtTiming = (lines, timingLineIndex) => { const timingMatch = SRT_TIMING_RE.exec(lines[timingLineIndex]?.trim() ?? ""); if (!timingMatch?.groups) return null; const startMs = parseClockTime(timingMatch.groups.start, 3); const endMs = parseClockTime(timingMatch.groups.end, 3); return startMs == null || endMs == null || endMs < startMs ? null : { startMs, endMs }; }; var readSrtPayload = (lines, startCursor) => { let cursor = startCursor; const payloadLines = []; while (cursor < lines.length) { if (lines[cursor].trim() === "") { if (isSrtCueStart(lines, cursor + 1)) break; cursor += 1; continue; } if (payloadLines.length > 0 && isSrtCueStart(lines, cursor)) break; payloadLines.push(lines[cursor]); cursor += 1; } return { rawText: trimEmptyBoundaryLines(payloadLines).join("\n"), nextCursor: cursor }; }; var createStyledCueDraft = (index, { rawText, startMs, endMs, speakerId, displayModel, metadata }) => ({ index, line: { text: displayModel.text, startMs, durationMs: getCueDurationMs(startMs, endMs), speakerId, tokens: [], metadata: { rawText, styledSpans: displayModel.styledSpans, ...metadata } } }); var createEmptyVttResult = () => ({ format: "vtt", subtitles: [], metadata: { vtt: { headerText: "", blocks: [] } } }); var isWebVttDocumentBlock = (line) => line.startsWith("NOTE") || line === "STYLE" || line === "REGION"; var readVttBlockLines = (lines, startCursor) => { const blockLines = []; let cursor = startCursor; while (cursor < lines.length && lines[cursor].trim() !== "") { blockLines.push(lines[cursor]); cursor += 1; } return { blockLines, nextCursor: cursor }; }; var resolveVttCueIdentity = (lines, cursor) => { if (!isVttTimingLine(lines[cursor] ?? "") && isVttTimingLine(lines[cursor + 1] ?? "")) return { cueId: lines[cursor], timingCursor: cursor + 1 }; return { cueId: void 0, timingCursor: cursor }; }; var parseVttTiming = (line) => { const timingParts = parseVttTimingParts(line); if (!timingParts) return null; const startMs = parseClockTime(timingParts.start, 3); const endMs = parseClockTime(timingParts.end, 3); if (startMs == null || endMs == null || endMs < startMs) return null; return { startMs, endMs, settingsRaw: timingParts.settings }; }; var readVttPayloadLines = (lines, startCursor) => { const payloadLines = []; let cursor = startCursor; while (cursor < lines.length && lines[cursor].trim() !== "") { payloadLines.push(lines[cursor]); cursor += 1; } return { payloadLines, nextCursor: cursor }; }; var parseAssEventFormatFields = (formatLine) => formatLine.slice(7).split(",").map((field) => field.trim()); var ensureAssEventFields = (eventFields, metadata) => eventFields.length || !metadata.eventFormat ? eventFields : parseAssEventFormatFields(metadata.eventFormat); var buildAssEventRecord = (eventFields, value) => { const values = splitAssFields(value, eventFields.length); return Object.fromEntries(eventFields.map((field, fieldIndex) => [field, values[fieldIndex] ?? ""])); }; var applyAssStyleSectionLine = (metadata, line) => { if (line.startsWith("Format:")) { metadata.styleFormat = line; return; } if (line.startsWith("Style:")) { metadata.styleLines.push(line); return; } metadata.preEventLines.push(line); }; var createAssCueDraft = (index, event) => { const startMs = parseAssTime(event.Start ?? ""); const endMs = parseAssTime(event.End ?? ""); if (startMs == null || endMs == null || endMs < startMs) return null; const rawText = event.Text ?? ""; const displayModel = buildStyledDisplayModel(rawText); const assMetadata = { kind: "dialogue", layer: event.Layer ?? "0", style: event.Style ?? "Default", name: event.Name ?? "", marginL: event.MarginL ?? "0", marginR: event.MarginR ?? "0", marginV: event.MarginV ?? "0", effect: event.Effect ?? "", rawText, overrideTags: extractAssOverrideTags(rawText) }; return { index, line: createStyledCueDraft(index, { rawText, startMs, endMs, speakerId: assMetadata.name || "0", displayModel, metadata: { ass: assMetadata } }).line }; }; var processAssEventLine = (metadata, line, index, eventFields) => { if (line.startsWith("Format:")) return { eventFields: parseAssEventFormatFields(line), cue: null }; if (!line.includes(":")) { metadata.preEventLines.push(line); return { eventFields, cue: null }; } const separatorIndex = line.indexOf(":"); const kind = line.slice(0, separatorIndex).trim(); const value = line.slice(separatorIndex + 1).trim(); const resolvedEventFields = ensureAssEventFields(eventFields, metadata); const event = buildAssEventRecord(resolvedEventFields, value); if (kind === "Comment") { metadata.commentLines.push(line); return { eventFields: resolvedEventFields, cue: null }; } if (kind !== "Dialogue") { metadata.preEventLines.push(line); return { eventFields: resolvedEventFields, cue: null }; } return { eventFields: resolvedEventFields, cue: createAssCueDraft(index, event) }; }; var parseSrt = (text) => { const lines = normalizeNewlines(text).split("\n"); const cues = []; let cursor = 0; let cueIndex = 0; while (cursor < lines.length) { while (cursor < lines.length && lines[cursor].trim() === "") cursor += 1; if (cursor >= lines.length) break; const timingLineIndex = getSrtTimingLineIndex(lines, cursor); const timing = parseSrtTiming(lines, timingLineIndex); if (!timing) { cursor += 1; continue; } const payload = readSrtPayload(lines, timingLineIndex + 1); cursor = payload.nextCursor; const rawText = payload.rawText; const displayModel = buildStyledDisplayModel(rawText); cues.push(createStyledCueDraft(cueIndex, { rawText, startMs: timing.startMs, endMs: timing.endMs, speakerId: "0", displayModel })); cueIndex += 1; } return { format: "srt", subtitles: toComparableSubtitleOrder(cues) }; }; var resolveSerializedText = (processed, line, format) => { if (processed.format === format) return line.metadata?.rawText ?? line.text; if (format === "ass") return line.text.replaceAll("\n", String.raw`\N`); return line.text; }; var serializeSrt = (processed) => processed.subtitles.map((line, index) => { const rawText = resolveSerializedText(processed, line, "srt"); const endMs = getSubtitleLineEndMs(line); return [ String(index + 1), `${formatClockTime(line.startMs, { delimiter: ",", allowOptionalHours: false, fractionDigits: 3 })} --> ${formatClockTime(endMs, { delimiter: ",", allowOptionalHours: false, fractionDigits: 3 })}`, rawText ].join("\n"); }).join("\n\n"); var pushWebVttBlock = (blocks, cueIndex, lines) => { blocks.push({ cueIndex, lines }); }; var parseVtt = (text) => { const lines = normalizeNewlines(text).split("\n"); const headerLine = lines[0] ?? ""; if (!headerLine.startsWith("WEBVTT")) return createEmptyVttResult(); const metadata = { headerText: headerLine.slice(6).trim(), blocks: [] }; const cues = []; let cursor = 1; while (cursor < lines.length) { while (cursor < lines.length && lines[cursor].trim() === "") cursor += 1; if (cursor >= lines.length) break; if (isWebVttDocumentBlock(lines[cursor])) { const block = readVttBlockLines(lines, cursor); cursor = block.nextCursor; pushWebVttBlock(metadata.blocks, cues.length, block.blockLines); continue; } const identity = resolveVttCueIdentity(lines, cursor); const timing = parseVttTiming(lines[identity.timingCursor] ?? ""); if (!timing) { cursor += 1; continue; } const payload = readVttPayloadLines(lines, identity.timingCursor + 1); cursor = payload.nextCursor; const payloadLines = payload.payloadLines; const rawText = payloadLines.join("\n"); const displayModel = buildStyledDisplayModel(rawText); const voice = extractVttVoice(rawText); cues.push({ index: cues.length, line: createStyledCueDraft(cues.length, { rawText, startMs: timing.startMs, endMs: timing.endMs, speakerId: voice ?? "0", displayModel, metadata: { vtt: { cueId: identity.cueId, settings: parseCueSettings(timing.settingsRaw), voice, rawPayload: payloadLines } } }).line }); } return { format: "vtt", subtitles: toComparableSubtitleOrder(cues), metadata: { vtt: metadata } }; }; var serializeVttTiming = (line) => { const endMs = getSubtitleLineEndMs(line); const settings = line.metadata?.vtt?.settings?.raw; const settingsSuffix = settings ? ` ${settings}` : ""; return `${formatClockTime(line.startMs, { delimiter: ".", allowOptionalHours: true, fractionDigits: 3 })} --> ${formatClockTime(endMs, { delimiter: ".", allowOptionalHours: true, fractionDigits: 3 })}${settingsSuffix}`; }; var serializeVtt = (processed) => { const metadata = processed.metadata?.vtt; const sections = [`WEBVTT${metadata?.headerText ? ` ${metadata.headerText}` : ""}`]; const blocksByIndex = /* @__PURE__ */ new Map(); for (const block of metadata?.blocks ?? []) { const existing = blocksByIndex.get(block.cueIndex) ?? []; existing.push(block.lines); blocksByIndex.set(block.cueIndex, existing); } const emitBlocks = (cueIndex) => { for (const lines of blocksByIndex.get(cueIndex) ?? []) sections.push(lines.join("\n")); }; emitBlocks(0); processed.subtitles.forEach((line, index) => { const cueId = line.metadata?.vtt?.cueId; const cueSections = [ ...cueId ? [cueId] : [], serializeVttTiming(line), (processed.format === "vtt" ? line.metadata?.vtt?.rawPayload : line.text.split("\n"))?.join("\n") ?? line.text ]; sections.push(cueSections.join("\n")); emitBlocks(index + 1); }); return sections.filter(Boolean).join("\n\n"); }; var splitAssFields = (value, fieldCount) => { if (fieldCount <= 1) return [value]; const fields = []; let cursor = 0; for (let i = 0; i < fieldCount - 1; i += 1) { const separatorIndex = value.indexOf(",", cursor); if (separatorIndex < 0) { fields.push(value.slice(cursor).trim()); cursor = value.length; break; } fields.push(value.slice(cursor, separatorIndex).trim()); cursor = separatorIndex + 1; } fields.push(value.slice(cursor).trim()); return fields; }; var createEmptyAssMetadata = () => ({ scriptInfoLines: [], styleFormat: "", styleLines: [], eventFormat: "", preEventLines: [], commentLines: [] }); var createDefaultAssMetadata = (title = "Exported subtitles") => ({ scriptInfoLines: [ `Title: ${title}`, "ScriptType: v4.00+", "WrapStyle: 0", "ScaledBorderAndShadow: yes" ], styleFormat: "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding", styleLines: ["Style: Default,Arial,42,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,0,0,0,0,100,100,0,0,1,2,0,2,20,20,20,1"], eventFormat: "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text", preEventLines: [], commentLines: [] }); var parseAss = (text) => { const lines = normalizeNewlines(text).split("\n"); const metadata = createEmptyAssMetadata(); const cues = []; let currentSection = ""; let eventFields = []; lines.forEach((line, index) => { const trimmed = line.trim(); if (!trimmed) return; const sectionMatch = /^\[(.+)\]$/u.exec(trimmed); if (sectionMatch) { currentSection = sectionMatch[1]; return; } if (currentSection === "Script Info") { metadata.scriptInfoLines.push(line); return; } if (currentSection === "V4+ Styles" || currentSection === "V4 Styles") { applyAssStyleSectionLine(metadata, line); return; } if (currentSection === "Events") { if (line.startsWith("Format:")) metadata.eventFormat = line; const result = processAssEventLine(metadata, line, index, eventFields); eventFields = result.eventFields; if (result.cue) cues.push(result.cue); } }); return { format: "ass", subtitles: toComparableSubtitleOrder(cues), metadata: { ass: metadata } }; }; var serializeAssDialogue = (line) => { const ass = line.metadata?.ass; const endMs = getSubtitleLineEndMs(line); const rawText = ass?.rawText ?? line.metadata?.rawText ?? line.text.replaceAll("\n", String.raw`\N`); return [ ass?.kind === "comment" ? "Comment" : "Dialogue", ": ", [ ass?.layer ?? "0", formatAssTime(line.startMs), formatAssTime(endMs), ass?.style ?? "Default", ass?.name ?? "", ass?.marginL ?? "0", ass?.marginR ?? "0", ass?.marginV ?? "0", ass?.effect ?? "", rawText ].join(",") ].join(""); }; var withAssTitle = (metadata, assTitle) => { if (!assTitle) return metadata; const titleLine = `Title: ${assTitle}`; const scriptInfoLines = [...metadata.scriptInfoLines]; const existingIndex = scriptInfoLines.findIndex((line) => line.startsWith("Title:")); if (existingIndex >= 0) { if (scriptInfoLines[existingIndex] === "Title: Exported subtitles") scriptInfoLines[existingIndex] = titleLine; } else scriptInfoLines.unshift(titleLine); return { ...metadata, scriptInfoLines }; }; var serializeAss = (processed, options) => { const sourceMetadata = processed.metadata?.ass; const metadata = withAssTitle(sourceMetadata && sourceMetadata.scriptInfoLines.length > 0 && sourceMetadata.styleFormat && sourceMetadata.styleLines.length > 0 && sourceMetadata.eventFormat ? sourceMetadata : createDefaultAssMetadata(options?.assTitle), options?.assTitle); return [ "[Script Info]", ...metadata.scriptInfoLines, "", "[V4+ Styles]", metadata.styleFormat, ...metadata.styleLines, ...metadata.preEventLines, "", "[Events]", metadata.eventFormat, ...metadata.commentLines, ...processed.subtitles.map((line) => serializeAssDialogue({ ...line, metadata: processed.format === "ass" ? line.metadata : { ...line.metadata, rawText: resolveSerializedText(processed, line, "ass") } })) ].join("\n").trim(); }; var parseSubtitleText = (text, format) => { if (format === "srt") return parseSrt(text); if (format === "vtt") return parseVtt(text); return parseAss(text); }; var sortProcessedSubtitles = (processed) => ({ ...processed, subtitles: toComparableSubtitleOrder(processed.subtitles.map((line, index) => ({ index, line }))) }); var toSubtitlesData = (processed) => { const subtitles = processed.subtitles.map((line) => ({ text: line.text, startMs: line.startMs, durationMs: line.durationMs, speakerId: line.speakerId, tokens: line.tokens.map((token) => ({ text: token.text, startMs: token.startMs, durationMs: token.durationMs })) })); return { containsTokens: subtitles.some((line) => line.tokens.length > 0), subtitles }; }; var serializeProcessedSubtitles = (processed, format, options) => { if (format === "json") return toSubtitlesData(processed); if (format === "srt") return serializeSrt(processed); if (format === "vtt") return serializeVtt(processed); return serializeAss(processed, options); }; //#endregion //#region src/videoHandler/translationVolume.ts function normalizeMediaElementVolume(volume) { return clampNumber(volume, 0, 1); } function normalizeGainVolume(volume) { return clampNumber(volume, 0, Infinity); } function setAudioParamInstant(param, value, context) { const now = context?.currentTime; if (typeof now === "number" && Number.isFinite(now)) { try { if (typeof param.cancelAndHoldAtTime === "function") param.cancelAndHoldAtTime(now); else if (typeof param.cancelScheduledValues === "function") param.cancelScheduledValues(now); } catch {} if (typeof param.setValueAtTime === "function") { param.setValueAtTime(value, now); return; } } param.value = value; } function safeSetPlayerVolume(player, volume) { const gainNode = player.gainNode; if (gainNode?.gain) { setAudioParamInstant(gainNode.gain, normalizeGainVolume(volume), gainNode.context); return; } player.volume = normalizeMediaElementVolume(volume); } function applyTranslationPlaybackVolume(player, volumePercent, fallbackVolumePercent) { const nextVolume = typeof volumePercent === "number" && Number.isFinite(volumePercent) ? volumePercent : fallbackVolumePercent; if (!player || typeof nextVolume !== "number" || !Number.isFinite(nextVolume)) return; safeSetPlayerVolume(player, nextVolume / 100); } //#endregion //#region src/ui/buttonPlacement.ts var SIDE_EDGE_FRACTION = .18; var SIDE_TOP_FRACTION = .36; var MIN_SIDE_EDGE_PX = 84; var MAX_SIDE_EDGE_PX = 220; var MIN_SIDE_TOP_PX = 140; var MAX_SIDE_TOP_PX = 280; function normalizeButtonPosition(position) { switch (position) { case "left": case "right": case "leftCenter": case "rightCenter": return position; default: return "default"; } } function isSideButtonPosition(position) { return position === "left" || position === "right" || position === "leftCenter" || position === "rightCenter"; } function getButtonDirection(position) { return isSideButtonPosition(position) ? "column" : "row"; } function resolveButtonLayout(isBigContainer, preferredPosition = "default") { const normalizedPosition = normalizeButtonPosition(preferredPosition); const position = isBigContainer || !isSideButtonPosition(normalizedPosition) ? normalizedPosition : "default"; return { position, direction: getButtonDirection(position) }; } function getEdgeSize(size, fraction, minPx, maxPx) { return clampNumber(size * fraction, minPx, maxPx); } function resolveSideVerticalPosition(side, y, height) { if (y <= getEdgeSize(height, SIDE_TOP_FRACTION, MIN_SIDE_TOP_PX, MAX_SIDE_TOP_PX)) return side; return side === "left" ? "leftCenter" : "rightCenter"; } function resolveButtonPositionFromPointer(clientX, clientY, containerRect, isBigContainer) { const width = containerRect.width; const height = containerRect.height; if (!(width > 0 && height > 0)) return "default"; const x = clampNumber(clientX - containerRect.left, 0, width); const y = clampNumber(clientY - containerRect.top, 0, height); if (!isBigContainer) return "default"; const sideEdge = getEdgeSize(width, SIDE_EDGE_FRACTION, MIN_SIDE_EDGE_PX, MAX_SIDE_EDGE_PX); if (x <= sideEdge) return resolveSideVerticalPosition("left", y, height); if (x >= width - sideEdge) return resolveSideVerticalPosition("right", y, height); return "default"; } //#endregion //#region src/ui/mount.ts /** * Compare overlay mount points by DOM identity. * * Mount updates should only run when one of the attachment roots actually * changes (root/portal/tooltip layout root). */ function isSameOverlayMount(previous, next) { return previous.root === next.root && previous.portalContainer === next.portalContainer && previous.subtitlesMountContainer === next.subtitlesMountContainer; } /** * Runs `onChanged` only when mount targets are actually different. * Returns the mount that should become current state. */ function applyOverlayMountUpdate(previous, next, onChanged) { if (isSameOverlayMount(previous, next)) return previous; onChanged(next); return next; } //#endregion //#region src/ui/translationCommands.ts async function getVideoDataForTranslation(videoHandler) { if (!videoHandler.videoData?.videoId) throw new VOTLocalizedError("VOTNoVideoIDFound"); if (shouldRefreshVideoDataBeforeTranslation(videoHandler)) videoHandler.videoData = await videoHandler.getVideoData(); if (!videoHandler.videoData?.videoId) throw new VOTLocalizedError("VOTNoVideoIDFound"); return videoHandler.videoData; } function shouldRefreshVideoDataBeforeTranslation(videoHandler) { return videoHandler.site.host === "vk" && videoHandler.site.additionalData === "clips" || videoHandler.site.host === "douyin"; } async function prepareAuthStateForTranslation(videoHandler) { if (!await deleteExpiredAccount(videoHandler)) return; openAuthWindow(); throw new VOTLocalizedError("VOTYandexTokenExpired"); } async function handleTranslationButtonCommand(deps) { const videoHandler = deps.videoHandler; if (!videoHandler) return; debug.log("[handleTranslationBtnClick] click translationBtn"); if (videoHandler.hasActiveSource()) { debug.log("[handleTranslationBtnClick] video has active source"); await videoHandler.stopTranslation(); return; } if (deps.currentStatus === "error" && !deps.currentLoading) deps.transformBtn("none", localizationProvider.get("translateVideo")); if (deps.currentStatus !== "none" || deps.currentLoading) { videoHandler.actionsAbortController.abort(); await videoHandler.stopTranslation(); return; } try { await prepareAuthStateForTranslation(videoHandler); debug.log("[handleTranslationBtnClick] trying execute translation"); const videoData = await getVideoDataForTranslation(videoHandler); if (videoHandler.autoSourceLanguageOverrideVideoId && videoHandler.autoSourceLanguageOverrideVideoId !== videoData.videoId) { videoHandler.translateFromLang = "auto"; videoHandler.autoSourceLanguageOverrideVideoId = void 0; videoHandler.setSelectMenuValues("auto", videoData.responseLanguage); } await videoHandler.videoManager.ensureDetectedLanguageForTranslation(videoData); debug.log("[handleTranslationBtnClick] Run translateFunc", videoData.videoId); const requestLang = videoHandler.translateFromLang === "auto" ? videoData.detectedLanguage : videoHandler.translateFromLang; await videoHandler.translateFunc(videoData.videoId, videoData.isStream, requestLang, videoData.responseLanguage, videoData.translationHelp); } catch (err) { if (isAbortError(err)) { deps.transformBtn("none", localizationProvider.get("translateVideo")); return; } console.error("[VOT]", err); if (!(err instanceof Error)) { deps.transformBtn("error", String(err)); return; } const message = err.name === "VOTLocalizedError" ? err.localizedMessage : err.message; deps.transformBtn("error", message); } } //#endregion //#region src/utils/inputDevice.ts function matchesMedia(query) { try { return typeof globalThis.matchMedia === "function" && globalThis.matchMedia(query).matches; } catch { return false; } } function hasTouchScreen() { return (globalThis.navigator?.maxTouchPoints ?? 0) > 0 || matchesMedia("(pointer: coarse)") || matchesMedia("(hover: none)"); } function isTouchFirstInput() { return matchesMedia("(pointer: coarse)") || hasTouchScreen() && !matchesMedia("(hover: hover)"); } //#endregion //#region src/ui/icons.ts var TRANSLATE_ICON_SVG = w` `; var PIP_ICON_SVG = w` `; var MENU_ICON = w` `; var DOWNLOAD_ICON = w` `; var SUBTITLES_ICON = w` `; var SETTINGS_ICON = w` `; var CHEVRON_ICON = w` `; var ARROW_RIGHT_ICON = w` `; var CLOSE_ICON = w` `; var WARNING_ICON = w` `; var HELP_ICON = w` `; var REFRESH_ICON = w` `; var KEY_ICON = w` `; /** * Animated equalizer icon for "Standard voices" (gray dots / bars). * Three bars that animate up and down like an audio waveform. */ var STANDARD_VOICE_ICON = w``; /** * Animated equalizer icon for "Live voices" (pink/magenta bars). * Five bars that animate like a lively audio equalizer. */ var LIVE_VOICE_ICON = w``; //#endregion //#region src/ui/components/downloadButton.ts var DownloadButton = class extends UIComponentWithEvents { loaderMain; loaderCircle; _progress = 0; constructor() { super(["click"]); const { container, loaderMain, loaderCircle } = this.createElements(); this.container = container; this.loaderMain = loaderMain; this.loaderCircle = loaderCircle; this.progress = 0; } createElements() { const container = UI.createIconButton(DOWNLOAD_ICON, { ariaLabel: "Download translation" }); const loaderMain = container.querySelector(".vot-loader-main"); if (!loaderMain) throw new Error("[VOT] DownloadButton loader main element not found"); const loaderCircle = container.querySelector(".vot-loader-progress"); if (!loaderCircle) throw new Error("[VOT] DownloadButton loader circle element not found"); container.addEventListener("click", () => { this.dispatch("click"); }); return { container, loaderMain, loaderCircle }; } get progress() { return this._progress; } set progress(value) { const normalized = clampProgress(value); this._progress = normalized; const circumference = this.getCircleCircumference(); this.loaderCircle.style.strokeDasharray = `${circumference}`; const offset = circumference * (1 - normalized / 100); this.loaderCircle.style.strokeDashoffset = `${offset}`; this.loaderMain.style.opacity = normalized === 0 ? "1" : "0"; this.loaderCircle.style.opacity = normalized === 0 ? "0" : "1"; } getCircleCircumference() { const radius = this.loaderCircle.r?.baseVal?.value ?? 0; return 2 * Math.PI * radius; } }; function clampProgress(value) { if (!Number.isFinite(value)) return 0; return clampPercentInt(value < 1 ? value * 100 : value); } //#endregion //#region src/ui/components/label.ts var Label = class extends UIComponent { icon; text; _labelText; _icon; constructor({ labelText, icon }) { super(); this._labelText = labelText; this._icon = icon; const { container, icon: iconEl, text } = this.createElements(); this.container = container; this.icon = iconEl; this.text = text; } createElements() { const container = UI.createEl("vot-block", ["vot-label"]); const text = UI.createEl("span", ["vot-label-text"]); text.textContent = this._labelText; const icon = UI.createEl("span", ["vot-label-icon"]); if (this._icon) D(this._icon, icon); else icon.hidden = true; container.append(text, icon); return { container, icon, text }; } }; //#endregion //#region src/ui/components/dialog.ts var Dialog = class extends UIComponentWithEvents { backdrop; box; contentWrapper; headerContainer; titleContainer; title; closeButton; bodyContainer; footerContainer; previouslyFocused = null; keydownListener; adaptiveAlignObserver; adaptiveAlignRaf = null; handleViewportChange = () => { this.scheduleAdaptiveVerticalAlign(); }; titleId = createDomId("vot-dialog-title"); _titleHtml; _isTemp; constructor({ titleHtml, isTemp = false }) { super(["close"]); this._titleHtml = titleHtml; this._isTemp = isTemp; const { container, backdrop, box, contentWrapper, headerContainer, titleContainer, title, closeButton, bodyContainer, footerContainer } = this.createElements(); this.container = container; this.backdrop = backdrop; this.box = box; this.contentWrapper = contentWrapper; this.headerContainer = headerContainer; this.titleContainer = titleContainer; this.title = title; this.closeButton = closeButton; this.bodyContainer = bodyContainer; this.footerContainer = footerContainer; } createElements() { const container = UI.createEl("vot-block", ["vot-dialog-container"]); if (this._isTemp) container.classList.add("vot-dialog-temp"); container.hidden = !this._isTemp; setInteractiveHiddenState(container, container.hidden); const backdrop = UI.createEl("vot-block", ["vot-dialog-backdrop"]); const box = UI.createEl("vot-block", ["vot-dialog"]); box.dataset.verticalAlign = "center"; box.setAttribute("role", "dialog"); box.setAttribute("aria-modal", "true"); box.tabIndex = -1; const contentWrapper = UI.createEl("vot-block", ["vot-dialog-content-wrapper"]); const headerContainer = UI.createEl("vot-block", ["vot-dialog-header-container"]); const titleContainer = UI.createEl("vot-block", ["vot-dialog-title-container"]); const title = UI.createEl("vot-block", ["vot-dialog-title"]); title.id = this.titleId; title.append(this._titleHtml); titleContainer.appendChild(title); box.setAttribute("aria-labelledby", this.titleId); const closeButton = UI.createIconButton(CLOSE_ICON, { ariaLabel: "Close" }); closeButton.classList.add("vot-dialog-close-button"); backdrop.addEventListener("click", (e) => { e.stopPropagation(); this.close(); }); closeButton.addEventListener("click", () => { this.close(); }); headerContainer.append(titleContainer, closeButton); const bodyContainer = UI.createEl("vot-block", ["vot-dialog-body-container"]); const footerContainer = UI.createEl("vot-block", ["vot-dialog-footer-container"]); contentWrapper.append(headerContainer, bodyContainer, footerContainer); box.appendChild(contentWrapper); container.append(backdrop, box); box.addEventListener("click", (e) => { e.stopPropagation(); }); return { container, backdrop, box, contentWrapper, headerContainer, titleContainer, title, closeButton, bodyContainer, footerContainer }; } open() { this.previouslyFocused ??= getDeepActiveElement(document); this.hidden = false; this.attachKeydownTrap(); this.attachAdaptiveVerticalAlign(); queueMicrotask(() => this.focusFirst()); return this; } remove() { this.detachAdaptiveVerticalAlign(); this.detachKeydownTrap(); this.container.remove(); this.restoreFocus(); this.dispatch("close"); return this; } close() { if (this._isTemp) return this.remove(); this.detachAdaptiveVerticalAlign(); this.detachKeydownTrap(); this.hidden = true; this.restoreFocus(); this.dispatch("close"); return this; } attachAdaptiveVerticalAlign() { if (this.adaptiveAlignObserver) { this.scheduleAdaptiveVerticalAlign(); return; } if (typeof ResizeObserver !== "undefined") { this.adaptiveAlignObserver = new ResizeObserver(() => { this.scheduleAdaptiveVerticalAlign(); }); this.adaptiveAlignObserver.observe(this.contentWrapper); } globalThis.addEventListener("resize", this.handleViewportChange, { passive: true }); if (globalThis.visualViewport) { globalThis.visualViewport.addEventListener("resize", this.handleViewportChange, { passive: true }); globalThis.visualViewport.addEventListener("scroll", this.handleViewportChange, { passive: true }); } this.scheduleAdaptiveVerticalAlign(); } detachAdaptiveVerticalAlign() { if (this.adaptiveAlignObserver) { this.adaptiveAlignObserver.disconnect(); this.adaptiveAlignObserver = void 0; } globalThis.removeEventListener("resize", this.handleViewportChange); globalThis.visualViewport?.removeEventListener("resize", this.handleViewportChange); globalThis.visualViewport?.removeEventListener("scroll", this.handleViewportChange); if (this.adaptiveAlignRaf !== null) { cancelAnimationFrame(this.adaptiveAlignRaf); this.adaptiveAlignRaf = null; } } scheduleAdaptiveVerticalAlign() { if (this.adaptiveAlignRaf !== null) cancelAnimationFrame(this.adaptiveAlignRaf); this.adaptiveAlignRaf = requestAnimationFrame(() => { this.adaptiveAlignRaf = null; this.updateAdaptiveVerticalAlign(); }); } updateAdaptiveVerticalAlign() { const viewportHeight = globalThis.visualViewport?.height ?? globalThis.innerHeight; if (!viewportHeight || viewportHeight <= 0) return; const centerMaxPx = Math.max(160, Math.round(viewportHeight * .75)); const topMaxPx = Math.max(160, Math.round(viewportHeight - 32)); const contentHeightPx = this.contentWrapper.scrollHeight; const currentlyTop = this.box.dataset.verticalAlign === "top"; const enterTopThresholdPx = centerMaxPx - 8; const exitTopThresholdPx = Math.round(viewportHeight * .6); if (currentlyTop ? contentHeightPx > exitTopThresholdPx : contentHeightPx >= enterTopThresholdPx) { this.box.dataset.verticalAlign = "top"; this.box.style.setProperty("--vot-dialog-max-height", `${topMaxPx}px`); } else { this.box.dataset.verticalAlign = "center"; this.box.style.setProperty("--vot-dialog-max-height", `${centerMaxPx}px`); } } restoreFocus() { const el = this.previouslyFocused; this.previouslyFocused = null; if (el && el instanceof HTMLElement && document.contains(el)) el.focus(); } getFocusableElements() { return Array.from(this.container.querySelectorAll([ "button:not([disabled])", "[href]", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])", "[role='button']:not([aria-disabled='true'])" ].join(","))).filter((el) => !el.hidden && el.getClientRects().length > 0); } focusFirst() { (this.getFocusableElements()[0] ?? this.closeButton ?? this.box).focus?.(); } attachKeydownTrap() { if (this.keydownListener) return; this.keydownListener = (e) => { if (e.key === "Escape") { e.preventDefault(); this.close(); return; } if (e.key !== "Tab") return; const focusables = this.getFocusableElements(); if (!focusables.length) { e.preventDefault(); this.box.focus(); return; } const first = focusables[0]; const last = focusables.at(-1) ?? first; const active = getDeepActiveElement(this.container.getRootNode()); if (e.shiftKey) { if (active === first || active === this.box) { e.preventDefault(); last.focus(); } } else if (active === last) { e.preventDefault(); first.focus(); } }; this.container.addEventListener("keydown", this.keydownListener); } detachKeydownTrap() { if (!this.keydownListener) return; this.container.removeEventListener("keydown", this.keydownListener); this.keydownListener = void 0; } set hidden(isHidden) { setInteractiveHiddenState(this.container, isHidden); } get hidden() { return super.hidden; } get isDialogOpen() { return !this.hidden; } }; //#endregion //#region src/ui/components/textfield.ts var Textfield = class extends UIComponentWithEvents { input; label; _labelHtml; _multiline; _placeholder; _value; constructor({ labelHtml = "", placeholder = "", value = "", multiline = false }) { super(["input", "change"]); this._labelHtml = labelHtml; this._multiline = multiline; this._placeholder = placeholder; this._value = value; const { container, input, label } = this.createElements(); this.container = container; this.input = input; this.label = label; } createElements() { const container = UI.createEl("vot-block", ["vot-textfield"]); const input = document.createElement(this._multiline ? "textarea" : "input"); if (!this._labelHtml) input.classList.add("vot-show-placeholer", "vot-show-placeholder"); input.placeholder = this._placeholder; input.value = this._value; const label = UI.createEl("span"); label.append(this._labelHtml); container.append(input, label); input.addEventListener("input", () => { this._value = this.input.value; this.dispatch("input", this._value); }); input.addEventListener("change", () => { this._value = this.input.value; this.dispatch("change", this._value); }); return { container, label, input }; } get value() { return this._value; } /** * If you set a different new value, it will trigger the change event */ set value(val) { if (this._value === val) return; this.input.value = this._value = val; this.dispatch("change", this._value); } get placeholder() { return this._placeholder; } set placeholder(text) { this.input.placeholder = this._placeholder = text; } get disabled() { return this.input.disabled; } set disabled(isDisabled) { this.input.disabled = isDisabled; } }; //#endregion //#region src/ui/components/select.ts var Select = class extends UIComponentWithEvents { outer; arrowIcon; title; dialogParent; labelElement; _selectTitle; _dialogTitle; multiSelect; baseItems; _items; searchItemsProvider; isLoading = false; isDialogOpen = false; searchRequestId = 0; contentList; contentItemSearchDatasetKey = "votSearchLabel"; contentItemIndexDatasetKey = "votIndex"; selectedItems = []; selectedValues; constructor({ selectTitle, dialogTitle, items, searchItemsProvider, labelElement, dialogParent = document.documentElement, multiSelect }) { super(["selectItem", "beforeOpen"]); this._selectTitle = selectTitle; this._dialogTitle = dialogTitle; this.baseItems = this.cloneItems(items); this._items = this.cloneItems(items); this.searchItemsProvider = searchItemsProvider; this.multiSelect = multiSelect ?? false; this.labelElement = labelElement; this.dialogParent = dialogParent; this.selectedValues = this.calcSelectedValues(); const { container, outer, arrowIcon, title } = this.createElements(); this.container = container; this.outer = outer; this.arrowIcon = arrowIcon; this.title = title; } cloneItems(items) { return items.map((item) => ({ ...item })); } static genLanguageItems(langs, conditionString) { return langs.map((lang) => { const phrase = `langs.${lang}`; const label = localizationProvider.get(phrase); return { label: label === phrase ? lang.toUpperCase() : label, value: lang, selected: conditionString === lang }; }); } multiSelectItemHandle = (item) => { const value = item.value; if (this.selectedValues.has(value) && this.selectedValues.size > 1) this.selectedValues.delete(value); else this.selectedValues.add(value); this.syncItemsSelectionState(); this.syncItemsSelectionState(this.baseItems); this.updateSelectedState(); this.dispatch("selectItem", Array.from(this.selectedValues)); }; singleSelectItemHandle = (item) => { const value = item.value; this.selectedValues = /* @__PURE__ */ new Set([value]); this.syncItemsSelectionState(); this.syncItemsSelectionState(this.baseItems); this.updateSelectedState(); this.dispatch("selectItem", value); }; onContentItemClick = (event) => { if (!(event.target instanceof HTMLElement)) return; const contentItem = event.target.closest(".vot-select-content-item"); if (!contentItem || contentItem.inert || !this.contentList?.contains(contentItem)) return; const rawIndex = contentItem.dataset[this.contentItemIndexDatasetKey]; if (!rawIndex) return; const item = this._items[Number(rawIndex)]; if (!item) return; if (this.multiSelect) { this.multiSelectItemHandle(item); return; } this.singleSelectItemHandle(item); }; syncItemsSelectionState(items = this._items) { for (const item of items) item.selected = this.selectedValues.has(item.value); } restoreBaseItems() { this._items = this.cloneItems(this.baseItems); this.syncItemsSelectionState(); this.updateSelectedState(); } createDialogContentList() { const contentList = UI.createEl("vot-block", ["vot-select-content-list"]); for (const [index, item] of this._items.entries()) { const contentItem = UI.createEl("vot-block", ["vot-select-content-item"]); contentItem.textContent = item.label; contentItem.dataset.votSelected = item.selected === true ? "true" : "false"; contentItem.dataset.votValue = item.value; contentItem.dataset[this.contentItemSearchDatasetKey] = item.label.toLowerCase(); contentItem.dataset[this.contentItemIndexDatasetKey] = String(index); if (item.disabled) contentItem.inert = true; contentList.appendChild(contentItem); } contentList.addEventListener("click", this.onContentItemClick); this.selectedItems = Array.from(contentList.children); return contentList; } createElements() { const container = UI.createEl("vot-block", ["vot-select"]); if (this.labelElement) { container.classList.add("vot-select--labeled"); container.append(this.labelElement); } else container.classList.add("vot-select--control-only"); const outer = UI.createEl("vot-block", ["vot-select-outer"]); UI.makeButtonLike(outer); outer.setAttribute("aria-haspopup", "dialog"); outer.setAttribute("aria-expanded", "false"); const title = UI.createEl("vot-block", ["vot-select-title"]); title.textContent = this.visibleText; const arrowIcon = UI.createEl("vot-block", ["vot-select-arrow-icon"]); D(CHEVRON_ICON, arrowIcon); outer.append(title, arrowIcon); outer.addEventListener("click", () => { if (this.disabled) return; if (this.isLoading || this.isDialogOpen) return; try { this.isLoading = true; const tempDialog = new Dialog({ titleHtml: this._dialogTitle, isTemp: true }); this.dispatch("beforeOpen", tempDialog); this.dialogParent.appendChild(tempDialog.container); this.isDialogOpen = true; outer.setAttribute("aria-expanded", "true"); const votSearchLangTextfield = new Textfield({ labelHtml: localizationProvider.get("searchField") }); votSearchLangTextfield.addEventListener("input", async (searchText) => { const requestId = ++this.searchRequestId; if (this.searchItemsProvider) { const providedItems = await this.searchItemsProvider(searchText); if (requestId !== this.searchRequestId) return; this.updateItems(providedItems, { persist: false }); } const normalizedSearchText = searchText.toLowerCase(); for (const contentItem of this.selectedItems) contentItem.hidden = !(contentItem.dataset[this.contentItemSearchDatasetKey] ?? "").includes(normalizedSearchText); }); this.contentList = this.createDialogContentList(); tempDialog.bodyContainer.append(votSearchLangTextfield.container, this.contentList); tempDialog.addEventListener("close", () => { this.isDialogOpen = false; this.restoreBaseItems(); this.selectedItems = []; this.contentList = void 0; outer.setAttribute("aria-expanded", "false"); }); tempDialog.open(); } finally { this.isLoading = false; } }); container.appendChild(outer); return { container, outer, arrowIcon, title }; } calcSelectedValues() { return new Set(this._items.filter((item) => item.selected).map((item) => item.value)); } updateTitle() { this.title.textContent = this.visibleText; return this; } updateSelectedState() { if (this.selectedItems.length > 0) for (const item of this.selectedItems) { const val = item.dataset.votValue; if (val === void 0) continue; item.dataset.votSelected = this.selectedValues.has(val).toString(); } this.updateTitle(); return this; } setSelectedValue(value) { const values = Array.isArray(value) ? value : [value]; let selectedValues; if (this.multiSelect) selectedValues = values; else selectedValues = values.length > 0 ? [values[0]] : []; this.selectedValues = new Set(selectedValues); this.syncItemsSelectionState(); this.syncItemsSelectionState(this.baseItems); this.updateSelectedState(); return this; } /** * @warning Use chaining with this method or reassign to variable to get the updated type of instance */ updateItems(newItems, options = {}) { const { persist = true } = options; const nextItems = this.cloneItems(newItems); if (persist) this.baseItems = this.cloneItems(nextItems); this._items = nextItems; this.selectedValues = this.calcSelectedValues(); this.updateSelectedState(); const dialogContainer = this.contentList?.parentElement; if (!this.contentList || !dialogContainer) return this; const oldContentList = this.contentList; this.contentList = this.createDialogContentList(); dialogContainer.replaceChild(this.contentList, oldContentList); return this; } get visibleText() { if (!this.multiSelect) return this._items.find((item) => item.selected)?.label ?? this._selectTitle; return this._items.filter((item) => this.selectedValues.has(item.value)).map((item) => item.label).join(", ") || this._selectTitle; } set selectTitle(title) { this._selectTitle = title; this.updateTitle(); } get disabled() { return this.outer.getAttribute("disabled") === "true" || this.outer.getAttribute("aria-disabled") === "true"; } set disabled(isDisabled) { this.outer.toggleAttribute("disabled", isDisabled); } }; //#endregion //#region src/ui/components/languagePairSelect.ts var LanguagePairSelect = class extends UIComponent { fromSelect; directionIcon; toSelect; dialogParent; _fromSelectTitle; _fromDialogTitle; _fromItems; _toSelectTitle; _toDialogTitle; _toItems; constructor({ from: { selectTitle: fromSelectTitle = localizationProvider.get("videoLanguage"), dialogTitle: fromDialogTitle = localizationProvider.get("videoLanguage"), items: fromItems }, to: { selectTitle: toSelectTitle = localizationProvider.get("translationLanguage"), dialogTitle: toDialogTitle = localizationProvider.get("translationLanguage"), items: toItems }, dialogParent = document.documentElement }) { super(); this._fromSelectTitle = fromSelectTitle; this._fromDialogTitle = fromDialogTitle; this._fromItems = fromItems; this._toSelectTitle = toSelectTitle; this._toDialogTitle = toDialogTitle; this._toItems = toItems; this.dialogParent = dialogParent; const { container, fromSelect, directionIcon, toSelect } = this.createElements(); this.container = container; this.fromSelect = fromSelect; this.directionIcon = directionIcon; this.toSelect = toSelect; } createElements() { const container = UI.createEl("vot-block", ["vot-lang-select"]); const fromSelect = new Select({ selectTitle: this._fromSelectTitle, dialogTitle: this._fromDialogTitle, items: this._fromItems, dialogParent: this.dialogParent }); const directionIcon = UI.createEl("vot-block", ["vot-lang-select-icon"]); D(ARROW_RIGHT_ICON, directionIcon); const toSelect = new Select({ selectTitle: this._toSelectTitle, dialogTitle: this._toDialogTitle, items: this._toItems, dialogParent: this.dialogParent }); container.append(fromSelect.container, directionIcon, toSelect.container); return { container, fromSelect, directionIcon, toSelect }; } setSelectedValues(from, to) { this.fromSelect.setSelectedValue(from); this.toSelect.setSelectedValue(to); return this; } updateItems(fromItems, toItems) { this._fromItems = fromItems; this._toItems = toItems; this.fromSelect = this.fromSelect.updateItems(fromItems); this.toSelect = this.toSelect.updateItems(toItems); return this; } }; //#endregion //#region src/ui/components/slider.ts var Slider = class extends UIComponentWithEvents { input; label; _labelHtml; _value; _min; _max; _step; constructor({ labelHtml, value = 50, min = 0, max = 100, step = 1 }) { super(["input"]); this._labelHtml = labelHtml; this._value = value; this._min = min; this._max = max; this._step = step; const { container, input, label } = this.createElements(); this.container = container; this.input = input; this.label = label; this.update(); } updateProgress() { const range = this._max - this._min; const progress = clampNumber(range <= 0 ? 0 : (this._value - this._min) / range, 0, 1); this.container.style.setProperty("--vot-progress", progress.toString()); return this; } update() { this._value = this.input.valueAsNumber; this._min = +this.input.min; this._max = +this.input.max; this.updateProgress(); return this; } createElements() { const container = UI.createEl("vot-block", ["vot-slider"]); const input = document.createElement("input"); input.type = "range"; input.min = this._min.toString(); input.max = this._max.toString(); input.step = this._step.toString(); input.value = this._value.toString(); const label = UI.createEl("span"); D(this._labelHtml, label); container.append(input, label); input.addEventListener("input", () => { this.update(); this.dispatch("input", this._value, false); }); return { container, label, input }; } get value() { return this._value; } /** * If you set a different new value, it will trigger the input event */ set value(val) { this._value = clampNumber(val, this._min, this._max); this.input.value = this._value.toString(); this.updateProgress(); this.dispatch("input", this._value, true); } get min() { return this._min; } set min(val) { this._min = val; this.input.min = this._min.toString(); this._value = clampNumber(this._value, this._min, this._max); this.input.value = this._value.toString(); this.updateProgress(); } get max() { return this._max; } set max(val) { this._max = val; this.input.max = this._max.toString(); this._value = clampNumber(this._value, this._min, this._max); this.input.value = this._value.toString(); this.updateProgress(); } get step() { return this._step; } set step(val) { this._step = val; this.input.step = this._step.toString(); } get disabled() { return this.input.disabled; } set disabled(isDisabled) { this.input.disabled = isDisabled; } }; //#endregion //#region src/ui/components/sliderLabel.ts var SliderLabel = class extends UIComponent { strong; text; _labelText; _labelEOL; _value; _symbol; constructor({ labelText, labelEOL = "", value = 50, symbol = "%" }) { super(); this._labelText = labelText; this._labelEOL = labelEOL; this._value = value; this._symbol = symbol; const { container, strong, text } = this.createElements(); this.container = container; this.strong = strong; this.text = text; } createElements() { const container = UI.createEl("vot-block", ["vot-slider-label"]); const text = UI.createEl("span", ["vot-slider-label-text"]); text.textContent = this.labelText; const strong = UI.createEl("span", ["vot-slider-label-value"]); strong.textContent = this.valueText; container.append(text, strong); return { container, strong, text }; } get labelText() { return `${this._labelText}${this._labelEOL}`; } get valueText() { return `${this._value}${this._symbol}`; } get value() { return this._value; } set value(val) { this._value = val; this.strong.textContent = this.valueText; } }; //#endregion //#region src/ui/components/voicePopover.ts var VoicePopover = class VoicePopover extends UIComponentWithEvents { id = createDomId("vot-voice-popover"); layoutRoot; _activeVoice; onTranslate; lastVisibilityState = false; showTimer = null; hideTimer = null; static SHOW_DELAY_MS = 80; static HIDE_DELAY_MS = 80; positionRafId = null; anchorEl = null; outsideTapHandler = null; layoutListening = false; onLayoutChangeBound = () => { if (this.isOpen && this.anchorEl) this.schedulePositionUpdate(this.anchorEl); }; constructor({ activeVoice, layoutRoot, onTranslate }) { super(["openChange", "voiceChange"]); this._activeVoice = activeVoice; this.layoutRoot = layoutRoot; this.onTranslate = onTranslate; this.container = this.createElements().container; } get activeVoice() { return this._activeVoice; } set activeVoice(value) { this._activeVoice = value; this.updateActiveState(); } set hidden(isHidden) { setInteractiveHiddenState(this.container, isHidden); } get hidden() { return super.hidden; } get isOpen() { return !this.hidden; } scheduleShow(anchor) { this.cancelHide(); this.cancelShow(); if (this.isOpen) { this.anchorEl = anchor; this.updatePosition(anchor); this.emitVisibilityChange(true); return; } this.showTimer = setTimeout(() => { this.showTimer = null; this.open(anchor); }, VoicePopover.SHOW_DELAY_MS); } scheduleHide() { this.cancelShow(); if (this.hidden) return; this.hideTimer = setTimeout(() => { this.hideTimer = null; this.close(); }, VoicePopover.HIDE_DELAY_MS); } showNow(anchor) { this.cancelShow(); this.cancelHide(); this.open(anchor); } toggle(anchor) { if (this.isOpen) this.hideNow(); else this.showNow(anchor); } toggleForTouch(anchor) { this.toggle(anchor); } cancelShow() { if (this.showTimer !== null) { clearTimeout(this.showTimer); this.showTimer = null; } } cancelHide() { if (this.hideTimer !== null) { clearTimeout(this.hideTimer); this.hideTimer = null; } } hideNow() { this.cancelShow(); this.cancelHide(); this.close(); } release() { this.cancelShow(); this.cancelHide(); this.close(); this.container.remove(); this.clearEventListeners(); } createElements() { const container = UI.createEl("vot-block", ["vot-voice-popover"]); container.id = this.id; container.setAttribute("role", "menu"); container.setAttribute("aria-label", "Voice type selection"); setInteractiveHiddenState(container, true); container.append(this.createItem("standard", STANDARD_VOICE_ICON, localizationProvider.get("VOTStandardVoicesTitle"), localizationProvider.get("VOTStandardVoicesSubtitle")), UI.createEl("vot-block", ["vot-voice-popover__divider"]), this.createItem("live", LIVE_VOICE_ICON, localizationProvider.get("VOTLiveVoicesTitle"), localizationProvider.get("VOTLiveVoicesSubtitle"))); container.addEventListener("pointerenter", (e) => { if (e.pointerType === "touch") return; this.cancelHide(); }); container.addEventListener("pointerleave", (e) => { if (e.pointerType === "touch") return; this.scheduleHide(); }); return { container }; } createItem(voice, iconTemplate, title, subtitle) { const item = UI.createEl("vot-block", ["vot-voice-popover__item"]); item.setAttribute("role", "menuitemradio"); item.setAttribute("tabindex", "0"); item.dataset.voice = voice; const iconWrap = UI.createEl("vot-block", ["vot-voice-popover__item-icon", `vot-voice-popover__item-icon--${voice}`]); D(iconTemplate, iconWrap); const textWrap = UI.createEl("vot-block", ["vot-voice-popover__item-text"]); const titleEl = UI.createEl("span", ["vot-voice-popover__item-title"]); titleEl.textContent = title; const subtitleEl = UI.createEl("span", ["vot-voice-popover__item-subtitle"]); subtitleEl.textContent = subtitle; textWrap.append(titleEl, subtitleEl); item.append(iconWrap, textWrap); const select = () => this.handleSelect(voice); item.addEventListener("pointerdown", (e) => { if (e.button !== 0 && e.pointerType !== "touch") return; e.preventDefault(); e.stopPropagation(); select(); }); item.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); select(); } }); return item; } open(anchor) { if (this.isOpen) { this.anchorEl = anchor; this.updatePosition(anchor); this.emitVisibilityChange(true); return; } this.anchorEl = anchor; this.hidden = false; this.updateActiveState(); this.updatePosition(anchor); this.attachLayoutListeners(); this.attachOutsideTapListener(); this.emitVisibilityChange(true); } close() { if (this.hidden) { this.emitVisibilityChange(false); return; } this.hidden = true; this.detachLayoutListeners(); this.detachOutsideTapListener(); this.anchorEl = null; if (this.positionRafId !== null) { cancelAnimationFrame(this.positionRafId); this.positionRafId = null; } this.emitVisibilityChange(false); } emitVisibilityChange(isOpen) { if (this.lastVisibilityState === isOpen) return; this.lastVisibilityState = isOpen; this.dispatch("openChange", isOpen); } handleSelect(voice) { this._activeVoice = voice; this.updateActiveState(); this.cancelHide(); this.dispatch("voiceChange", voice); this.onTranslate?.(); this.hideNow(); } updateActiveState() { for (const item of this.container.querySelectorAll(".vot-voice-popover__item")) { const isActive = item.dataset.voice === this._activeVoice; item.classList.toggle("vot-voice-popover__item--active", isActive); item.setAttribute("aria-checked", isActive.toString()); } } schedulePositionUpdate(anchor) { if (this.positionRafId !== null) return; this.positionRafId = requestAnimationFrame(() => { this.positionRafId = null; this.updatePosition(anchor); }); } positionColumn(containerRect, rootRect, gap, position) { const spaceLeft = containerRect.left - rootRect.left - gap; const spaceRight = rootRect.right - containerRect.right - gap; const placement = (position === "right" || position === "rightCenter") && spaceLeft >= 160 || spaceLeft >= spaceRight ? "left" : "right"; this.container.style.setProperty("--vot-voice-popover-max-width", `${Math.max(160, Math.min(310, placement === "left" ? spaceLeft : spaceRight))}px`); const popoverRect = this.container.getBoundingClientRect(); const top = containerRect.top + containerRect.height / 2 - popoverRect.height / 2; return { left: placement === "left" ? containerRect.left - popoverRect.width - gap : containerRect.right + gap, top, placement }; } positionRow(containerRect, rootRect, gap) { const spaceAbove = containerRect.top - rootRect.top - gap; const spaceBelow = rootRect.bottom - containerRect.bottom - gap; const placement = spaceAbove >= spaceBelow ? "top" : "bottom"; this.container.style.setProperty("--vot-voice-popover-max-height", `${Math.max(96, placement === "top" ? spaceAbove : spaceBelow)}px`); const popoverRect = this.container.getBoundingClientRect(); return { left: containerRect.left + containerRect.width / 2 - popoverRect.width / 2, top: placement === "top" ? containerRect.top - popoverRect.height - gap : containerRect.bottom + gap, placement }; } updatePosition(anchor) { if (!this.isOpen) return; const rootRect = this.layoutRoot.getBoundingClientRect(); const gap = 8; const maxRootWidth = Math.max(160, rootRect.width - 16); const maxRootHeight = Math.max(96, rootRect.height - 16); this.container.style.setProperty("--vot-voice-popover-max-width", `${Math.min(310, maxRootWidth)}px`); this.container.style.setProperty("--vot-voice-popover-max-height", `${maxRootHeight}px`); const buttonContainer = anchor.closest("[data-direction]") ?? anchor; const containerRect = buttonContainer.getBoundingClientRect(); const direction = buttonContainer.dataset?.direction ?? "row"; const position = buttonContainer.dataset?.position ?? "default"; const result = direction === "column" ? this.positionColumn(containerRect, rootRect, gap, position) : this.positionRow(containerRect, rootRect, gap); const popoverRect = this.container.getBoundingClientRect(); let left = Math.max(gap, Math.min(result.left, rootRect.right - popoverRect.width - gap)); let top = Math.max(gap, Math.min(result.top, rootRect.bottom - popoverRect.height - gap)); left -= rootRect.left; top -= rootRect.top; this.container.dataset.placement = result.placement; this.container.style.left = `${left}px`; this.container.style.top = `${top}px`; } attachLayoutListeners() { if (this.layoutListening) return; this.layoutListening = true; window.addEventListener("scroll", this.onLayoutChangeBound, true); window.addEventListener("resize", this.onLayoutChangeBound); window.visualViewport?.addEventListener("scroll", this.onLayoutChangeBound); window.visualViewport?.addEventListener("resize", this.onLayoutChangeBound); } detachLayoutListeners() { if (!this.layoutListening) return; this.layoutListening = false; window.removeEventListener("scroll", this.onLayoutChangeBound, true); window.removeEventListener("resize", this.onLayoutChangeBound); window.visualViewport?.removeEventListener("scroll", this.onLayoutChangeBound); window.visualViewport?.removeEventListener("resize", this.onLayoutChangeBound); } attachOutsideTapListener() { this.detachOutsideTapListener(); this.outsideTapHandler = (e) => { if (isEventInside(e, this.container)) return; if (this.anchorEl && isEventInside(e, this.anchorEl)) return; this.hideNow(); }; document.addEventListener("pointerdown", this.outsideTapHandler, { capture: true, passive: true }); } detachOutsideTapListener() { if (!this.outsideTapHandler) return; document.removeEventListener("pointerdown", this.outsideTapHandler, { capture: true }); this.outsideTapHandler = null; } }; //#endregion //#region src/ui/components/votButton.ts function isSidePosition(position) { return position === "left" || position === "right" || position === "leftCenter" || position === "rightCenter"; } var VOTButton = class extends UIComponent { translateButton; dropdownArrow; separator; subtitlesButton; separator3; pipButton; separator2; menuButton; label; _opacity = 1; _position; _direction; _status; _subtitlesActive = false; /** Text shown next to the translate icon (plain text, not HTML). */ _labelText; constructor({ position = "default", direction = "default", status = "none", labelHtml = "" }) { super(); this._position = position; this._direction = direction; this._status = status; this._labelText = labelHtml; const { container, translateButton, dropdownArrow, separator, subtitlesButton, separator3, pipButton, separator2, menuButton, label } = this.createElements(); this.container = container; this.translateButton = translateButton; this.dropdownArrow = dropdownArrow; this.separator = separator; this.subtitlesButton = subtitlesButton; this.separator3 = separator3; this.pipButton = pipButton; this.separator2 = separator2; this.menuButton = menuButton; this.label = label; } createElements() { const container = UI.createEl("vot-block", ["vot-segmented-button"]); container.dataset.position = this._position; container.dataset.direction = this._direction; container.dataset.status = this._status; const translateButton = UI.createEl("vot-block", ["vot-segment", "vot-translate-button"]); translateButton.setAttribute("role", "button"); translateButton.tabIndex = 0; translateButton.setAttribute("aria-label", this._labelText || "Translate"); D(TRANSLATE_ICON_SVG, translateButton); const label = UI.createEl("span", ["vot-segment-label"]); label.textContent = this._labelText; const dropdownArrow = UI.createEl("span", ["vot-dropdown-arrow"]); dropdownArrow.setAttribute("role", "button"); dropdownArrow.tabIndex = 0; dropdownArrow.setAttribute("aria-label", localizationProvider.get("VOTVoiceSelection")); dropdownArrow.setAttribute("aria-haspopup", "menu"); dropdownArrow.setAttribute("aria-expanded", "false"); D(CHEVRON_ICON, dropdownArrow); translateButton.append(label, dropdownArrow); const separator = UI.createEl("vot-block", ["vot-separator"]); const subtitlesButton = UI.createEl("vot-block", ["vot-segment-only-icon", "vot-subtitles-button"]); subtitlesButton.setAttribute("role", "button"); subtitlesButton.tabIndex = 0; const subtitlesLabelText = localizationProvider.get("VOTSubtitles"); subtitlesButton.setAttribute("aria-label", subtitlesLabelText); subtitlesButton.setAttribute("aria-pressed", "false"); subtitlesButton.dataset.active = "false"; D(SUBTITLES_ICON, subtitlesButton); const separator3 = UI.createEl("vot-block", ["vot-separator"]); const pipButton = UI.createEl("vot-block", ["vot-segment-only-icon"]); pipButton.setAttribute("role", "button"); pipButton.tabIndex = 0; pipButton.setAttribute("aria-label", "Picture in picture"); D(PIP_ICON_SVG, pipButton); const separator2 = UI.createEl("vot-block", ["vot-separator"]); const menuButton = UI.createEl("vot-block", ["vot-segment-only-icon"]); menuButton.setAttribute("role", "button"); menuButton.tabIndex = 0; menuButton.setAttribute("aria-label", "Menu"); menuButton.setAttribute("aria-haspopup", "dialog"); menuButton.setAttribute("aria-expanded", "false"); D(MENU_ICON, menuButton); container.append(translateButton, separator, subtitlesButton, separator3, pipButton, separator2, menuButton); return { container, translateButton, dropdownArrow, separator, subtitlesButton, separator3, pipButton, separator2, menuButton, label }; } showSubtitlesButton(visible) { this.separator3.hidden = this.subtitlesButton.hidden = !visible; return this; } showPiPButton(visible) { this.separator2.hidden = this.pipButton.hidden = !visible; return this; } setText(labelText) { this._labelText = labelText; this.label.textContent = labelText; this.translateButton.setAttribute("aria-label", labelText || "Translate"); return this; } remove() { this.container.remove(); return this; } get tooltipPos() { switch (this.position) { case "left": case "leftCenter": return "right"; case "right": case "rightCenter": return "left"; default: return "bottom"; } } set status(status) { this._status = this.container.dataset.status = status; } get status() { return this._status; } set loading(isLoading) { this.container.dataset.loading = isLoading.toString(); } get loading() { return this.container.dataset.loading === "true"; } get subtitlesActive() { return this._subtitlesActive; } set subtitlesActive(isActive) { this._subtitlesActive = isActive; this.subtitlesButton.dataset.active = isActive.toString(); this.subtitlesButton.setAttribute("aria-pressed", isActive.toString()); } setVoiceMenuOpen(isOpen) { this.dropdownArrow.setAttribute("aria-expanded", isOpen.toString()); this.dropdownArrow.classList.toggle("vot-dropdown-arrow--open", isOpen); this.translateButton.classList.toggle("vot-translate-button--voice-menu-open", isOpen); return this; } get position() { return this._position; } set position(position) { this._position = this.container.dataset.position = position; } get direction() { return this._direction; } set direction(direction) { this._direction = this.container.dataset.direction = direction; } /** * Voice popover chevron is shown only in the centered horizontal layout. * In side layout the whole translate icon is the voice-popover handle. */ syncDropdownArrowPlacement() { const onSide = isSidePosition(this._position); this.dropdownArrow.hidden = onSide; this.dropdownArrow.setAttribute("aria-hidden", onSide.toString()); this.dropdownArrow.tabIndex = onSide ? -1 : 0; if (this.dropdownArrow.parentElement !== this.translateButton) this.translateButton.appendChild(this.dropdownArrow); } set opacity(opacity) { const o = Number.isFinite(opacity) ? opacity : 1; this._opacity = o; const isHidden = o <= .01; this.container.classList.toggle("vot-segmented-button--hidden", isHidden); } get opacity() { return this._opacity; } }; //#endregion //#region src/ui/components/votMenu.ts var VOTMenu = class extends UIComponent { contentWrapper; headerContainer; bodyContainer; footerContainer; titleContainer; title; _position; _titleHtml; menuId = createDomId("vot-menu"); titleId = createDomId("vot-menu-title"); constructor({ position = "default", titleHtml = "" }) { super(); this._position = position; this._titleHtml = titleHtml; const { container, contentWrapper, headerContainer, bodyContainer, footerContainer, titleContainer, title } = this.createElements(); this.container = container; this.contentWrapper = contentWrapper; this.headerContainer = headerContainer; this.bodyContainer = bodyContainer; this.footerContainer = footerContainer; this.titleContainer = titleContainer; this.title = title; } createElements() { const container = UI.createEl("vot-block", ["vot-menu"]); container.hidden = true; container.id = this.menuId; container.dataset.position = this._position; container.setAttribute("role", "dialog"); container.setAttribute("aria-modal", "false"); setInteractiveHiddenState(container, true); const contentWrapper = UI.createEl("vot-block", ["vot-menu-content-wrapper"]); container.appendChild(contentWrapper); const headerContainer = UI.createEl("vot-block", ["vot-menu-header-container"]); const titleContainer = UI.createEl("vot-block", ["vot-menu-title-container"]); headerContainer.appendChild(titleContainer); const title = UI.createEl("vot-block", ["vot-menu-title"]); title.id = this.titleId; title.append(this._titleHtml); titleContainer.appendChild(title); container.setAttribute("aria-labelledby", this.titleId); const bodyContainer = UI.createEl("vot-block", ["vot-menu-body-container"]); const footerContainer = UI.createEl("vot-block", ["vot-menu-footer-container"]); contentWrapper.append(headerContainer, bodyContainer, footerContainer); return { container, contentWrapper, headerContainer, bodyContainer, footerContainer, titleContainer, title }; } setText(titleText) { this._titleHtml = this.title.textContent = titleText; return this; } remove() { this.container.remove(); return this; } set hidden(isHidden) { setInteractiveHiddenState(this.container, isHidden); } get hidden() { return super.hidden; } get position() { return this._position; } set position(position) { this._position = this.container.dataset.position = position; } }; //#endregion //#region src/ui/views/overlay.ts var OverlayView = class OverlayView { static BIG_CONTAINER_WIDTH_PX = 550; resizeObserver; lastIsBigContainer = false; fullscreenHelper; mount; globalPortal; abortController = null; defaultVolumePersistTimer; defaultVolumePersistDelayMs = 250; dragThresholdPx = 6; dragActionSuppressMs = 350; dragState = null; dockPreview = null; lastButtonDragEndAt = 0; initialized = false; data; videoHandler; intervalIdleChecker; overlayMount; events = { "click:settings": new EventImpl(), "click:pip": new EventImpl(), "click:subtitles": new EventImpl(), "click:downloadTranslation": new EventImpl(), "click:downloadSubtitles": new EventImpl(), "click:translate": new EventImpl(), "input:videoVolume": new EventImpl(), "input:translationVolume": new EventImpl(), "select:fromLanguage": new EventImpl(), "select:toLanguage": new EventImpl(), "select:subtitles": new EventImpl(), "select:voiceType": new EventImpl() }; votButton; votButtonTooltip; subtitlesButtonTooltip; voiceMenuButtonTooltip; voicePopover; votMenu; downloadTranslationButton; downloadSubtitlesButton; openSettingsButton; languagePairSelect; subtitlesSelectLabel; subtitlesSelect; videoVolumeSliderLabel; videoVolumeSlider; translationVolumeSliderLabel; translationVolumeSlider; constructor({ mount, globalPortal, data = {}, videoHandler, intervalIdleChecker }) { this.mount = mount; this.globalPortal = globalPortal; this.data = data; this.videoHandler = videoHandler; this.intervalIdleChecker = intervalIdleChecker; this.fullscreenHelper = new FullscreenHelper({ container: videoHandler?.container || mount.root, video: videoHandler?.video }); } get root() { return this.overlayMount?.root ?? this.mount.root; } get portalContainer() { return this.mount.portalContainer; } get tooltipParentElement() { return this.root instanceof ShadowRoot ? this.root.host : this.root; } /** * Update mount points when the player container changes. * Moves already-mounted UI nodes and rebinds root-bound listeners (dragging). */ updateMount(nextMount) { const prevRoot = this.mount.root; const nextRoot = nextMount.root; this.mount = nextMount; if (!this.isInitialized()) return this; if (prevRoot !== nextRoot && this.overlayMount) reparentShadowMount(this.overlayMount, nextRoot); if (prevRoot !== nextRoot) for (const tooltip of [ this.votButtonTooltip, this.subtitlesButtonTooltip, this.voiceMenuButtonTooltip ]) tooltip?.updateMount({ parentElement: this.tooltipParentElement }); return this; } isInitialized() { return this.initialized; } calcButtonLayout(position) { return resolveButtonLayout(this.isBigContainer, position); } /** Centered bar uses dropdown arrow; side layout uses translate segment hover. */ isCenteredButtonLayout() { return this.votButton?.direction !== "column"; } /** Side layout blocks voice popover on error (tooltip instead). Centered bar always allows dropdown. */ allowsVoicePopover() { if (!this.votButton) return false; if (this.isCenteredButtonLayout()) return true; return this.votButton.status !== "error"; } shouldUseTouchVoiceInteraction(event) { return event?.pointerType === "touch" || isTouchFirstInput(); } shouldUseHoverVoiceInteraction(event) { return event?.pointerType !== "touch" && !isTouchFirstInput(); } /** Error tooltip only on side layout — centered bar uses dropdown + button label. */ syncTranslateButtonTooltip() { if (!this.isInitialized()) return this; const tooltip = this.votButtonTooltip; const showErrorTooltip = this.votButton.status === "error" && !this.isCenteredButtonLayout(); tooltip.hidden = !showErrorTooltip; tooltip.dismissImmediate(); if (showErrorTooltip) requestAnimationFrame(() => { requestAnimationFrame(() => tooltip.revealIfHovered()); }); return this; } /** * When status leaves `error`, pointer may still sit on translate segment or * chevron — no new `pointerenter` fires, so re-arm the hover popover here. */ rescheduleVoicePopoverIfHovered() { if (!this.isInitialized() || isTouchFirstInput()) return this; requestAnimationFrame(() => { requestAnimationFrame(() => { if (!this.isInitialized() || !this.allowsVoicePopover()) return; const vp = this.voicePopover; if (!vp || vp.isOpen) return; const hovered = (el) => { if (!el?.isConnected) return false; try { return el.matches(":hover"); } catch { return false; } }; if (this.votButton.direction === "column") { if (hovered(this.votButton.translateButton)) vp.scheduleShow(this.votButton.translateButton); } else if (hovered(this.votButton.dropdownArrow)) vp.scheduleShow(this.votButton.dropdownArrow); }); }); return this; } addEventListener(type, listener) { this.events[type].addListener(listener); return this; } removeEventListener(type, listener) { this.events[type].removeListener(listener); return this; } scheduleDefaultVolumePersist() { if (this.defaultVolumePersistTimer !== void 0) globalThis.clearTimeout(this.defaultVolumePersistTimer); this.defaultVolumePersistTimer = globalThis.setTimeout(() => { this.defaultVolumePersistTimer = void 0; this.flushDefaultVolumePersist(); }, this.defaultVolumePersistDelayMs); } bindPrimaryAction(element, handler, signal, options = {}) { element.addEventListener("pointerup", (event) => { if (!isPrimaryPointerAction(event)) return; if (this.shouldSuppressPointerAction()) return; if (options.shouldHandlePointer?.(event) === false) return; if (options.preventPointerDefault) event.preventDefault(); handler(); queueMicrotask(() => this.queueButtonAutoHideAfterInteraction()); }, { signal }); addKeyboardActivationListener(element, () => { handler(); queueMicrotask(() => this.queueButtonAutoHideAfterInteraction()); }, { signal }); } flushDefaultVolumePersist() { if (this.defaultVolumePersistTimer !== void 0) { globalThis.clearTimeout(this.defaultVolumePersistTimer); this.defaultVolumePersistTimer = void 0; } if (typeof this.data.defaultVolume !== "number") return; votStorage.set("defaultVolume", this.data.defaultVolume); } initUI(buttonPosition = "default") { if (this.isInitialized()) throw new Error("[VOT] OverlayView is already initialized"); this.initialized = true; this.lastIsBigContainer = this.isBigContainer; this.overlayMount = createShadowMount({ parent: this.mount.root, rootClasses: ["vot-overlay-root"], hostStyles: { position: "absolute", inset: "0", display: "block", "pointer-events": "none" }, rootStyles: { position: "relative", display: "block", width: "100%", height: "100%", "pointer-events": "none" } }); const { position, direction } = this.calcButtonLayout(buttonPosition); this.votButton = new VOTButton({ position, direction, status: "none", labelHtml: localizationProvider.get("translateVideo") }); this.votButton.opacity = 0; if (!this.pipButtonVisible) this.votButton.showPiPButton(false); this.votButton.showSubtitlesButton(true); this.root.appendChild(this.votButton.container); this.votButton.syncDropdownArrowPlacement(); this.votButtonTooltip = new Tooltip({ target: this.votButton.translateButton, content: localizationProvider.get("translateVideo"), position: this.votButton.tooltipPos, autoLayout: false, hidden: true, bordered: false, parentElement: this.tooltipParentElement }); const activeVoice = this.data.useLivelyVoice === false ? "standard" : "live"; this.voicePopover = new VoicePopover({ activeVoice, layoutRoot: this.root, onTranslate: () => { this.events["click:translate"].dispatch(); } }); this.voicePopover.addEventListener("openChange", (isOpen) => { this.votButton?.setVoiceMenuOpen(isOpen); }); this.votButton.container.dataset.voiceType = activeVoice; this.root.appendChild(this.voicePopover.container); this.syncTranslateButtonTooltip(); this.subtitlesButtonTooltip = new Tooltip({ target: this.votButton.subtitlesButton, content: localizationProvider.get("VOTSubtitles"), position: this.votButton.tooltipPos, autoLayout: false, bordered: false, parentElement: this.tooltipParentElement }); this.voiceMenuButtonTooltip = new Tooltip({ target: this.votButton.dropdownArrow, anchor: this.votButton.dropdownArrow, edgeAnchor: this.votButton.translateButton, content: localizationProvider.get("VOTVoiceSelection"), position: this.votButton.tooltipPos, autoLayout: false, bordered: false, parentElement: this.tooltipParentElement }); this.voiceMenuButtonTooltip.hidden = this.votButton.dropdownArrow.hidden === true; this.votMenu = new VOTMenu({ titleHtml: localizationProvider.get("VOTSettings"), position }); this.root.appendChild(this.votMenu.container); this.setupResizeObserver(); this.votButton.menuButton.setAttribute("aria-controls", this.votMenu.container.id); this.downloadTranslationButton = new DownloadButton(); this.downloadTranslationButton.hidden = true; this.downloadSubtitlesButton = UI.createIconButton(SUBTITLES_ICON, { ariaLabel: "Download subtitles" }); this.downloadSubtitlesButton.hidden = true; this.openSettingsButton = UI.createIconButton(SETTINGS_ICON, { ariaLabel: localizationProvider.get("VOTSettings") }); this.votMenu.headerContainer.append(this.downloadTranslationButton.container, this.downloadSubtitlesButton, this.openSettingsButton); const detectedLanguage = this.videoHandler?.videoData?.detectedLanguage ?? "en"; const responseLanguage = this.data.responseLanguage ?? "ru"; this.languagePairSelect = new LanguagePairSelect({ from: { selectTitle: localizationProvider.get(`langs.${detectedLanguage}`), items: Select.genLanguageItems(availableLangs, detectedLanguage) }, to: { selectTitle: localizationProvider.get(`langs.${responseLanguage}`), items: Select.genLanguageItems(availableTTS, responseLanguage) }, dialogParent: this.globalPortal }); this.subtitlesSelectLabel = new Label({ labelText: localizationProvider.get("VOTSubtitles") }); this.subtitlesSelect = new Select({ selectTitle: localizationProvider.get("VOTSubtitlesDisabled"), dialogTitle: localizationProvider.get("VOTSubtitles"), labelElement: this.subtitlesSelectLabel.container, dialogParent: this.globalPortal, items: [{ label: localizationProvider.get("VOTSubtitlesDisabled"), value: "disabled", selected: true }] }); const videoVolume = this.videoHandler ? this.videoHandler.getVideoVolume() * 100 : 100; this.videoVolumeSliderLabel = new SliderLabel({ labelText: localizationProvider.get("VOTVolume"), value: videoVolume }); this.videoVolumeSlider = new Slider({ labelHtml: this.videoVolumeSliderLabel.container, value: videoVolume }); this.videoVolumeSlider.hidden = !this.data.showVideoSlider || this.votButton.status !== "success"; const defaultVolume = this.data.defaultVolume ?? 100; this.translationVolumeSliderLabel = new SliderLabel({ labelText: localizationProvider.get("VOTVolumeTranslation"), value: defaultVolume }); this.translationVolumeSlider = new Slider({ labelHtml: this.translationVolumeSliderLabel.container, value: defaultVolume, max: this.data.audioBooster && !this.data.syncVolume ? 900 : 100 }); this.translationVolumeSlider.hidden = this.votButton.status !== "success"; this.votMenu.bodyContainer.append(this.languagePairSelect.container, this.subtitlesSelect.container, this.videoVolumeSlider.container, this.translationVolumeSlider.container); return this; } initUIEvents() { if (!this.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized"); this.abortController = new AbortController(); const signal = this.abortController.signal; this.votButton.container.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, { signal }); const setMenuOpen = (open, { returnFocusToToggle = false } = {}) => { if (!this.isInitialized()) return; this.votMenu.hidden = !open; this.votButton.menuButton.setAttribute("aria-expanded", open.toString()); if (open) queueMicrotask(() => this.openSettingsButton?.focus?.()); else if (returnFocusToToggle) queueMicrotask(() => this.votButton.menuButton.focus?.()); else this.votButton.menuButton.blur(); }; const toggleMenu = () => setMenuOpen(this.votMenu.hidden); const closeMenu = (returnFocusToToggle = false) => setMenuOpen(false, { returnFocusToToggle }); const toggleVoicePopover = () => { if (!this.allowsVoicePopover()) return; const arrow = this.votButton.dropdownArrow; this.voiceMenuButtonTooltip?.dismissImmediate(); if (this.voicePopover?.isOpen) { this.voicePopover.hideNow(); this.votButton.setVoiceMenuOpen(false); queueMicrotask(() => this.queueButtonAutoHideAfterInteraction()); return; } this.voicePopover?.showNow(arrow); }; this.votButton.dropdownArrow.addEventListener("pointerdown", (event) => { if (!isPrimaryPointerAction(event)) return; event.preventDefault(); event.stopPropagation(); if (this.shouldSuppressPointerAction()) return; toggleVoicePopover(); }, { signal, capture: true }); this.votButton.dropdownArrow.addEventListener("pointerup", (event) => { event.preventDefault(); event.stopPropagation(); }, { signal, capture: true }); this.votButton.dropdownArrow.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); }, { signal, capture: true }); this.votButton.dropdownArrow.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); event.stopPropagation(); toggleVoicePopover(); }, { signal, capture: true }); this.bindPrimaryAction(this.votButton.translateButton, () => { closeMenu(); this.events["click:translate"].dispatch(); }, signal, { shouldHandlePointer: (event) => !(this.votButton.direction === "column" && this.allowsVoicePopover() && this.shouldUseTouchVoiceInteraction(event)) }); this.votButton.translateButton.addEventListener("pointerenter", (e) => { if (!this.shouldUseHoverVoiceInteraction(e)) return; if (this.votButton.direction !== "column") return; if (!this.allowsVoicePopover()) return; this.voicePopover?.scheduleShow(this.votButton.translateButton); }, { signal }); this.votButton.translateButton.addEventListener("pointerleave", (e) => { if (!this.shouldUseHoverVoiceInteraction(e)) return; if (this.votButton.direction !== "column") return; this.voicePopover?.scheduleHide(); }, { signal }); this.votButton.translateButton.addEventListener("pointerup", (e) => { if (!this.shouldUseTouchVoiceInteraction(e)) return; if (this.votButton.direction !== "column") return; if (this.shouldSuppressPointerAction()) return; if (!this.allowsVoicePopover()) return; e.preventDefault(); e.stopPropagation(); if (this.voicePopover?.isOpen) { this.voicePopover.hideNow(); this.votButton.setVoiceMenuOpen(false); queueMicrotask(() => this.queueButtonAutoHideAfterInteraction()); return; } this.voicePopover?.showNow(this.votButton.translateButton); }, { signal }); this.voicePopover.addEventListener("voiceChange", (voice) => { const useLive = voice === "live"; if (this.data.useLivelyVoice === useLive) { queueMicrotask(() => this.queueButtonAutoHideAfterInteraction()); return; } this.data.useLivelyVoice = useLive; votStorage.set("useLivelyVoice", useLive); this.syncVoicePopoverState(); this.events["select:voiceType"].dispatch(useLive); queueMicrotask(() => this.queueButtonAutoHideAfterInteraction()); }); this.bindPrimaryAction(this.votButton.pipButton, () => { closeMenu(); this.events["click:pip"].dispatch(); }, signal); this.bindPrimaryAction(this.votButton.subtitlesButton, () => { closeMenu(); this.events["click:subtitles"].dispatch(); }, signal); this.bindPrimaryAction(this.votButton.menuButton, toggleMenu, signal, { preventPointerDefault: true }); const touchAction = "none"; this.votButton.container.style.touchAction = touchAction; this.votButton.translateButton.style.touchAction = touchAction; this.votButton.dropdownArrow.style.touchAction = touchAction; this.votButton.subtitlesButton.style.touchAction = touchAction; this.votButton.pipButton.style.touchAction = touchAction; this.votButton.menuButton.style.touchAction = touchAction; this.votButton.container.addEventListener("pointerdown", this.onButtonDragPointerDown, { signal }); document.addEventListener("pointermove", this.onButtonDragPointerMove, { signal, capture: true }); document.addEventListener("pointerup", this.onButtonDragPointerUp, { signal, capture: true }); document.addEventListener("pointercancel", this.onButtonDragPointerCancel, { signal, capture: true }); this.votButton.container.addEventListener("lostpointercapture", this.onButtonDragPointerCancel, { signal }); this.votMenu.container.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, { signal }); for (const event of ["pointerdown"]) this.votMenu.container.addEventListener(event, (e) => { e.stopImmediatePropagation(); }, { signal }); document.addEventListener("pointerdown", (e) => { if (this.votMenu.hidden) return; const isInsideDialog = (typeof e.composedPath === "function" ? e.composedPath() : []).some((node) => node instanceof HTMLElement && node.classList.contains("vot-dialog-container")); if (isEventInside(e, this.votMenu.container) || isEventInside(e, this.votButton.menuButton) || isEventInside(e, this.votButton.container) || isInsideDialog) return; closeMenu(false); }, { signal, capture: true, passive: true }); this.votMenu.container.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; const keyboardNav = document.documentElement.classList.contains("vot-keyboard-nav"); e.preventDefault(); e.stopPropagation(); closeMenu(keyboardNav); if (!(this.votButton.container.matches(":hover") || this.votMenu.container.matches(":hover"))) this.videoHandler?.overlayVisibility?.queueAutoHide?.(); }, { signal }); this.downloadTranslationButton.addEventListener("click", () => { this.events["click:downloadTranslation"].dispatch(); }); this.downloadSubtitlesButton.addEventListener("click", () => { this.events["click:downloadSubtitles"].dispatch(); }, { signal }); this.openSettingsButton.addEventListener("click", () => { closeMenu(); this.events["click:settings"].dispatch(); }, { signal }); this.languagePairSelect.fromSelect.addEventListener("selectItem", (language) => { if (this.videoHandler?.videoData) { this.videoHandler.translateFromLang = language; this.videoHandler.videoData.detectedLanguage = language; this.videoHandler.videoManager.rememberUserLanguageSelection(this.videoHandler.videoData.videoId, language); } this.events["select:fromLanguage"].dispatch(language); }); this.languagePairSelect.toSelect.addEventListener("selectItem", async (language) => { if (this.videoHandler?.videoData) this.videoHandler.translateToLang = this.videoHandler.videoData.responseLanguage = language; const prevResponseLanguage = this.data.responseLanguage; if (prevResponseLanguage !== language) { this.data.responseLanguage = language; await votStorage.set("responseLanguage", this.data.responseLanguage); } if (this.data.enabledDontTranslateLanguages && Array.isArray(this.data.dontTranslateLanguages) && this.data.dontTranslateLanguages.length === 1 && prevResponseLanguage !== language && typeof prevResponseLanguage === "string" && this.data.dontTranslateLanguages[0] === prevResponseLanguage) { this.data.dontTranslateLanguages = [language]; await votStorage.set("dontTranslateLanguages", this.data.dontTranslateLanguages); } this.events["select:toLanguage"].dispatch(language); }); this.subtitlesSelect.addEventListener("beforeOpen", async (dialog) => { if (!this.videoHandler?.videoData) return; const subtitleLanguage = this.videoHandler.getPreferredSubtitlesLanguage(this.videoHandler.videoData.detectedLanguage, this.videoHandler.videoData.responseLanguage); if (!subtitleLanguage) return; const cacheKey = this.videoHandler.getSubtitlesCacheKey(this.videoHandler.videoData.videoId, this.videoHandler.videoData.detectedLanguage, subtitleLanguage); if (this.videoHandler.subtitlesCacheKey === cacheKey) return; if (this.videoHandler.cacheManager.getSubtitles(cacheKey) !== void 0) { await this.videoHandler.ensureSubtitlesForCurrentLangPair(); return; } const prevLoading = this.votButton?.loading ?? false; if (this.votButton) this.votButton.loading = true; const loadingEl = UI.createInlineLoader(); loadingEl.style.margin = "0 auto"; dialog.footerContainer.appendChild(loadingEl); try { await this.videoHandler.ensureSubtitlesForCurrentLangPair(); } finally { loadingEl.remove(); if (this.votButton) this.votButton.loading = prevLoading; } }); this.subtitlesSelect.addEventListener("selectItem", (data) => { this.events["select:subtitles"].dispatch(data); }); this.videoVolumeSlider.addEventListener("input", (value, fromSetter) => { if (this.videoVolumeSliderLabel) this.videoVolumeSliderLabel.value = value; if (fromSetter) return; this.events["input:videoVolume"].dispatch(value); }); this.translationVolumeSlider.addEventListener("input", (value, fromSetter) => { if (this.translationVolumeSliderLabel) this.translationVolumeSliderLabel.value = value; if (this.data.defaultVolume !== value) { this.data.defaultVolume = value; this.scheduleDefaultVolumePersist(); } if (fromSetter) return; this.events["input:translationVolume"].dispatch(value); }); return this; } updateButtonLayout(position, direction, options = {}) { if (!this.isInitialized()) return this; this.votMenu.position = position; this.votButton.position = position; this.votButton.direction = direction; this.votButton.syncDropdownArrowPlacement(); this.votButtonTooltip.setPosition(this.votButton.tooltipPos); this.subtitlesButtonTooltip.setPosition(this.votButton.tooltipPos); this.voiceMenuButtonTooltip.setPosition(this.votButton.tooltipPos); this.voiceMenuButtonTooltip.hidden = this.votButton.dropdownArrow.hidden === true; if (!options.keepVoicePopover && this.voicePopover?.isOpen) { this.voicePopover.hideNow(); this.voiceMenuButtonTooltip?.dismissImmediate(); this.votButton.setVoiceMenuOpen(false); } this.syncTranslateButtonTooltip(); return this; } /** Sync the voice popover's active state with the current data. */ syncVoicePopoverState() { if (!this.isInitialized()) return this; const activeVoice = this.data.useLivelyVoice === false ? "standard" : "live"; this.voicePopover.activeVoice = activeVoice; this.votButton.container.dataset.voiceType = activeVoice; return this; } syncSubtitlesButtonState(isActive) { if (!this.isInitialized()) return this; const active = isActive ?? Array.from(this.subtitlesSelect?.selectedValues ?? []).some((value) => value !== "disabled"); this.votButton.subtitlesActive = active; return this; } getOverlayRootElement() { const root = this.tooltipParentElement; return root instanceof ShadowRoot ? root.host : root; } shouldSuppressPointerAction() { return Boolean(this.dragState?.active) || Date.now() - this.lastButtonDragEndAt < this.dragActionSuppressMs; } closeFloatingButtonUI() { if (!this.isInitialized()) return; this.votMenu.hidden = true; this.votButton.menuButton.setAttribute("aria-expanded", "false"); this.voicePopover?.hideNow(); this.voiceMenuButtonTooltip?.dismissImmediate(); this.votButton.setVoiceMenuOpen(false); } isElementHovered(element) { if (!element?.isConnected) return false; try { return element.matches(":hover"); } catch { return false; } } getFloatingInteractionTargets() { if (!this.isInitialized()) return []; return [ this.votButton.container, this.votMenu.container, this.voicePopover.container ].filter((element) => element.isConnected); } isKeyboardFocusWithinFloatingUI() { if (typeof document === "undefined" || typeof document.hasFocus !== "function" || !document.hasFocus() || !document.documentElement.classList.contains("vot-keyboard-nav")) return false; const active = getDeepActiveElement(document); if (!(active instanceof Node)) return false; return this.getFloatingInteractionTargets().some((target) => containsCrossShadow(target, active)); } shouldKeepVisibleForInteraction() { if (!this.isInitialized()) return false; const hoverActive = !isTouchFirstInput() && this.getFloatingInteractionTargets().some((target) => this.isElementHovered(target)); return this.hasOpenFloatingButtonUI() || hoverActive || this.isKeyboardFocusWithinFloatingUI(); } blurPointerFocusInsideButton() { if (!this.isInitialized()) return; if (document.documentElement.classList.contains("vot-keyboard-nav")) return; const active = getDeepActiveElement(document); if (active instanceof HTMLElement && containsCrossShadow(this.votButton.container, active)) active.blur(); } hasOpenFloatingButtonUI() { if (!this.isInitialized()) return false; return !this.votMenu.hidden || Boolean(this.voicePopover?.isOpen); } queueButtonAutoHideAfterInteraction() { if (!this.isInitialized()) return; if (this.shouldKeepVisibleForInteraction()) { this.videoHandler?.overlayVisibility?.cancel?.(); return; } this.blurPointerFocusInsideButton(); if (this.shouldKeepVisibleForInteraction()) { this.videoHandler?.overlayVisibility?.cancel?.(); return; } this.videoHandler?.overlayVisibility?.queueAutoHide?.(); } ensureDockPreview() { if (!this.isInitialized()) return null; if (this.dockPreview?.isConnected) return this.dockPreview; const preview = this.votButton.container.cloneNode(true); preview.classList.add("vot-segmented-button--dock-preview"); preview.classList.remove("vot-segmented-button--dragging", "vot-segmented-button--hidden"); preview.removeAttribute("id"); preview.removeAttribute("aria-grabbed"); preview.setAttribute("aria-hidden", "true"); preview.querySelectorAll("[tabindex]").forEach((element) => { element.tabIndex = -1; }); this.root.appendChild(preview); this.dockPreview = preview; return preview; } removeDockPreview() { this.dockPreview?.remove(); this.dockPreview = null; } syncDockPreview(position, direction) { const preview = this.ensureDockPreview(); if (!preview) return; preview.dataset.position = position; preview.dataset.direction = direction; preview.dataset.status = this.votButton.status; preview.dataset.loading = this.votButton.loading.toString(); preview.dataset.dragTarget = "true"; preview.classList.toggle("vot-segmented-button--dock-preview-side", direction === "column"); preview.querySelectorAll("[aria-expanded]").forEach((element) => { element.setAttribute("aria-expanded", "false"); }); const arrow = preview.querySelector(".vot-dropdown-arrow"); if (arrow) { arrow.hidden = direction === "column"; arrow.setAttribute("aria-hidden", (direction === "column").toString()); } } updateDraggingButtonPosition() { const state = this.dragState; if (!this.isInitialized() || !state?.active) return; const rootRect = this.getOverlayRootElement().getBoundingClientRect(); state.rootRect = rootRect; const buttonRect = this.votButton.container.getBoundingClientRect(); const maxLeft = Math.max(0, rootRect.width - buttonRect.width); const maxTop = Math.max(0, rootRect.height - buttonRect.height); const nextLeft = Math.max(0, Math.min(state.clientX - rootRect.left - buttonRect.width / 2, maxLeft)); const nextTop = Math.max(0, Math.min(state.clientY - rootRect.top - buttonRect.height / 2, maxTop)); this.votButton.container.style.setProperty("--vot-button-drag-left", `${nextLeft}px`); this.votButton.container.style.setProperty("--vot-button-drag-top", `${nextTop}px`); } startActiveButtonDrag() { if (!this.isInitialized() || !this.dragState?.active) return; this.closeFloatingButtonUI(); this.updateDraggingButtonPosition(); this.votButton.container.classList.add("vot-segmented-button--dragging"); this.votButton.container.dataset.dragging = "true"; this.votButton.container.setAttribute("aria-grabbed", "true"); this.updateDragTarget(resolveButtonPositionFromPointer(this.dragState.clientX, this.dragState.clientY, this.dragState.rootRect, this.isBigContainer)); } updateDragTarget(position) { if (!this.isInitialized() || !this.dragState) return; const { position: layoutPosition, direction } = resolveButtonLayout(this.isBigContainer, position); if (this.dragState.targetPosition !== layoutPosition) this.dragState.targetPosition = layoutPosition; this.syncDockPreview(layoutPosition, direction); } applyButtonDragFrame() { const state = this.dragState; if (!this.isInitialized() || !state?.active) return; state.frameId = null; this.updateDraggingButtonPosition(); const nextPosition = resolveButtonPositionFromPointer(state.clientX, state.clientY, state.rootRect, this.isBigContainer); this.updateDragTarget(nextPosition); } requestButtonDragFrame() { const state = this.dragState; if (!state?.active || state.frameId !== null) return; state.frameId = requestAnimationFrame(() => this.applyButtonDragFrame()); } finishButtonDrag(commit) { const state = this.dragState; if (!state) return; if (state.frameId !== null) { cancelAnimationFrame(state.frameId); state.frameId = null; } const pointerId = state.pointerId; const wasActive = state.active; const shouldCommit = commit && wasActive && this.isInitialized(); const targetPosition = shouldCommit ? state.targetPosition : state.initialPosition; if (wasActive) this.lastButtonDragEndAt = Date.now(); this.dragState = null; try { if (this.votButton?.container.hasPointerCapture(pointerId)) this.votButton.container.releasePointerCapture(pointerId); } catch {} if (this.isInitialized()) { this.votButton.container.classList.remove("vot-segmented-button--dragging"); delete this.votButton.container.dataset.dragging; this.votButton.container.style.removeProperty("--vot-button-drag-left"); this.votButton.container.style.removeProperty("--vot-button-drag-top"); this.votButton.container.removeAttribute("aria-grabbed"); this.removeDockPreview(); if (wasActive) { const { position, direction } = this.calcButtonLayout(targetPosition); this.updateButtonLayout(position, direction); if (shouldCommit) { this.data.buttonPos = targetPosition; votStorage.set("buttonPos", targetPosition); } this.queueButtonAutoHideAfterInteraction(); } } else this.removeDockPreview(); } beginButtonDragCandidate(event) { if (!this.isInitialized()) return; const rootRect = this.getOverlayRootElement().getBoundingClientRect(); const initialPosition = normalizeButtonPosition(this.data.buttonPos ?? this.votButton.position); this.dragState = { pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, clientX: event.clientX, clientY: event.clientY, rootRect, active: false, initialPosition, targetPosition: initialPosition, frameId: null }; this.intervalIdleChecker.markActivity("overlay-button-drag-start"); } onButtonDragPointerDown = (event) => { if (!event.isPrimary || event.button !== 0 || this.dragState) return; this.beginButtonDragCandidate(event); }; onButtonDragPointerMove = (event) => { const state = this.dragState; if (state?.pointerId !== event.pointerId) return; state.clientX = event.clientX; state.clientY = event.clientY; if (!state.active) { if (Math.hypot(event.clientX - state.startClientX, event.clientY - state.startClientY) < this.dragThresholdPx) return; state.active = true; try { this.votButton?.container.setPointerCapture(event.pointerId); } catch {} this.startActiveButtonDrag(); } event.preventDefault(); event.stopPropagation(); this.intervalIdleChecker.markActivity("overlay-button-drag-move"); this.requestButtonDragFrame(); }; onButtonDragPointerUp = (event) => { const state = this.dragState; if (state?.pointerId !== event.pointerId) return; state.clientX = event.clientX; state.clientY = event.clientY; if (state.active) { event.preventDefault(); event.stopImmediatePropagation(); this.applyButtonDragFrame(); } this.finishButtonDrag(true); }; onButtonDragPointerCancel = (event) => { const state = this.dragState; if (state?.pointerId !== event.pointerId) return; if (state.active) { event.preventDefault(); event.stopImmediatePropagation(); } this.finishButtonDrag(false); }; updateButtonOpacity(opacity) { if (!this.isInitialized() || !this.votMenu.hidden) return this; const nextOpacity = opacity <= .01 && this.voicePopover?.isOpen && hasTouchScreen() ? 1 : opacity; if (Math.abs(this.votButton.opacity - nextOpacity) > .01) { this.votButton.opacity = nextOpacity; if (nextOpacity <= .01) this.voicePopover?.hideNow(); } return this; } doReleaseUI() { this.votButton?.remove(); this.votMenu?.remove(); this.votButtonTooltip?.release(); this.subtitlesButtonTooltip?.release(); this.voiceMenuButtonTooltip?.release(); this.voicePopover?.release(); if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = void 0; } this.fullscreenHelper.destroy(); destroyShadowMount(this.overlayMount); this.overlayMount = void 0; } doReleaseUIEvents() { this.abortController?.abort(); this.abortController = null; this.finishButtonDrag(false); this.flushDefaultVolumePersist(); for (const event of Object.values(this.events)) event.clear(); } release() { if (!this.isInitialized()) return this; this.doReleaseUIEvents(); this.doReleaseUI(); this.initialized = false; return this; } get isBigContainer() { return this.fullscreenHelper.isBigContainer(OverlayView.BIG_CONTAINER_WIDTH_PX); } setupResizeObserver() { if (this.resizeObserver) return; this.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const { width } = entry.contentRect; const currentIsBigContainer = width > OverlayView.BIG_CONTAINER_WIDTH_PX; if (this.lastIsBigContainer !== currentIsBigContainer) { this.lastIsBigContainer = currentIsBigContainer; this.handleContainerSizeChange(currentIsBigContainer); } this.updateMenuHeight(entry.contentRect.height); } }); const target = this.fullscreenHelper.getResizeObserverTarget(); this.resizeObserver.observe(target); } updateMenuHeight(containerHeight) { if (!this.isInitialized() || !this.votMenu?.container) return; let height; if (containerHeight && containerHeight > 200) height = containerHeight; else { const target = this.fullscreenHelper.getResizeObserverTarget(); height = target.getBoundingClientRect().height || target.clientHeight || window.innerHeight * .75; } if (!height || height < 200) height = window.innerHeight * .75; this.votMenu.container.style.setProperty("--vot-container-height", `${height}px`); } handleContainerSizeChange(isBigContainer) { if (!this.isInitialized()) return; const { position, direction } = resolveButtonLayout(isBigContainer, normalizeButtonPosition(this.data.buttonPos ?? this.votButton.position)); if (position !== this.votButton.position || direction !== this.votButton.direction) this.updateButtonLayout(position, direction); } get pipButtonVisible() { return isPiPAvailable() && !!this.data.showPiPButton; } }; //#endregion //#region src/types/components/votButton.ts var positions = [ "default", "left", "right", "leftCenter", "rightCenter" ]; //#endregion //#region src/videoHandler/shared.ts /** * Country code used for proxy settings. Populated lazily during init. */ var _countryCode; function getCountryCode() { return _countryCode; } function setCountryCode(next) { _countryCode = next; } //#endregion //#region src/ui/components/accountButton.ts var DEFAULT_AVATAR_ID = "0/0-0"; var DEFAULT_USERNAME = "unnamed"; var AccountButton = class extends UIComponentWithEvents { accountWrapper; buttons; usernameEl; avatarEl; avatarImg; actionButton; refreshButton; tokenButton; _loggedIn; _username; _avatarId; constructor({ loggedIn = false, username = DEFAULT_USERNAME, avatarId = DEFAULT_AVATAR_ID } = {}) { super([ "click", "click:secret", "refresh" ]); this._loggedIn = loggedIn; this._username = username; this._avatarId = avatarId; const { container, accountWrapper, buttons, usernameEl, avatarEl, avatarImg, actionButton, refreshButton, tokenButton } = this.createElements(); this.container = container; this.accountWrapper = accountWrapper; this.buttons = buttons; this.usernameEl = usernameEl; this.avatarEl = avatarEl; this.avatarImg = avatarImg; this.actionButton = actionButton; this.refreshButton = refreshButton; this.tokenButton = tokenButton; } createElements() { const container = UI.createEl("vot-block", ["vot-account"]); const accountWrapper = UI.createEl("vot-block", ["vot-account-wrapper"]); accountWrapper.hidden = !this._loggedIn; const avatarImg = UI.createEl("img", ["vot-account-avatar-img"]); avatarImg.src = `${avatarServerUrl}/${this._avatarId}/islands-retina-middle`; avatarImg.loading = "lazy"; avatarImg.alt = "user avatar"; const avatarEl = UI.createEl("vot-block", ["vot-account-avatar"], avatarImg); const usernameEl = UI.createEl("vot-block", ["vot-account-username"]); usernameEl.textContent = this._username; accountWrapper.append(avatarEl, usernameEl); const buttons = UI.createEl("vot-block", ["vot-account-buttons"]); const actionButton = UI.createOutlinedButton(this.buttonText); actionButton.addEventListener("click", () => { this.dispatch("click"); }); const tokenButton = UI.createIconButton(KEY_ICON, { ariaLabel: localizationProvider.get("VOTLoginViaToken") }); tokenButton.hidden = this._loggedIn; tokenButton.addEventListener("click", () => { this.dispatch("click:secret"); }); const refreshButton = UI.createIconButton(REFRESH_ICON, { ariaLabel: localizationProvider.get("VOTRefresh") }); refreshButton.addEventListener("click", () => { this.dispatch("refresh"); }); buttons.append(actionButton, tokenButton, refreshButton); container.append(accountWrapper, buttons); return { container, accountWrapper, buttons, usernameEl, avatarImg, avatarEl, actionButton, refreshButton, tokenButton }; } get buttonText() { return this._loggedIn ? localizationProvider.get("VOTLogout") : localizationProvider.get("VOTLogin"); } get loggedIn() { return this._loggedIn; } set loggedIn(isLoggedIn) { this._loggedIn = isLoggedIn; this.accountWrapper.hidden = !this._loggedIn; this.actionButton.textContent = this.buttonText; this.tokenButton.hidden = this._loggedIn; } get avatarId() { return this._avatarId; } set avatarId(avatarId) { this._avatarId = avatarId ?? DEFAULT_AVATAR_ID; this.avatarImg.src = `${avatarServerUrl}/${this._avatarId}/islands-retina-middle`; } get username() { return this._username; } set username(username) { this._username = username ?? DEFAULT_USERNAME; this.usernameEl.textContent = this._username; } }; //#endregion //#region src/ui/components/checkbox.ts var Checkbox = class extends UIComponentWithEvents { input; label; _labelHtml; _checked; _isSubCheckbox; constructor({ labelHtml, checked = false, isSubCheckbox = false }) { super(["change"]); this._labelHtml = labelHtml; this._checked = checked; this._isSubCheckbox = isSubCheckbox; const { container, input, label } = this.createElements(); this.container = container; this.input = input; this.label = label; } createElements() { const container = UI.createEl("label", ["vot-checkbox"]); if (this._isSubCheckbox) container.classList.add("vot-checkbox-sub"); const input = document.createElement("input"); input.type = "checkbox"; input.checked = this._checked; input.addEventListener("change", () => { this._checked = input.checked; this.dispatch("change", this._checked); }); const label = UI.createEl("span"); D(this._labelHtml, label); container.append(input, label); return { container, input, label }; } get disabled() { return this.input.disabled; } set disabled(isDisabled) { this.input.disabled = isDisabled; } get checked() { return this._checked; } /** * If you set a different new value, it will trigger the change event */ set checked(isChecked) { if (this._checked === isChecked) return; this._checked = this.input.checked = isChecked; this.dispatch("change", this._checked); } }; //#endregion //#region src/ui/components/details.ts var Details = class extends UIComponentWithEvents { header; arrowIcon; _titleHtml; constructor({ titleHtml }) { super(["click"]); this._titleHtml = titleHtml; const { container, header, arrowIcon } = this.createElements(); this.container = container; this.header = header; this.arrowIcon = arrowIcon; } createElements() { const container = UI.createEl("vot-block", ["vot-details"]); UI.makeButtonLike(container); const header = UI.createEl("vot-block"); header.append(this._titleHtml); const arrowIcon = UI.createEl("vot-block", ["vot-details-arrow-icon"]); D(CHEVRON_ICON, arrowIcon); container.append(header, arrowIcon); container.addEventListener("click", () => { this.dispatch("click"); }); return { container, header, arrowIcon }; } }; //#endregion //#region src/ui/components/hotkeyButton.ts var HotkeyButton = class extends UIComponentWithEvents { button; _labelHtml; _key; pressedKeys; comboKeys; recording = false; constructor({ labelHtml, key = null }) { super(["change"]); this._labelHtml = labelHtml; this._key = key; this.pressedKeys = /* @__PURE__ */ new Set(); this.comboKeys = /* @__PURE__ */ new Set(); const { container, button } = this.createElements(); this.container = container; this.button = button; } stopRecordingKeys() { this.recording = false; document.removeEventListener("keydown", this.keydownHandle); document.removeEventListener("keyup", this.keyupOrBlurHandle); globalThis.removeEventListener("blur", this.blurHandle); delete this.button.dataset.status; this.pressedKeys.clear(); this.comboKeys.clear(); } keydownHandle = (event) => { if (!this.recording || event.repeat) return; event.preventDefault(); if (event.code === "Escape") { this.key = null; this.button.textContent = this.keyText; this.stopRecordingKeys(); return; } this.pressedKeys.add(event.code); this.comboKeys.add(event.code); this.button.textContent = formatKeysComboDisplay(this.pressedKeys); }; keyupOrBlurHandle = (event) => { if (!this.recording) return; if (event) { this.pressedKeys.delete(event.code); this.button.textContent = this.pressedKeys.size ? formatKeysComboDisplay(this.pressedKeys) : formatKeysComboDisplay(this.comboKeys); if (this.pressedKeys.size) return; } this.key = this.comboKeys.size ? formatKeysCombo(this.comboKeys) : null; this.stopRecordingKeys(); }; blurHandle = () => { this.keyupOrBlurHandle(); }; buttonClickHandle = () => { if (this.recording) { this.stopRecordingKeys(); this.button.textContent = this.keyText; return; } this.button.dataset.status = "active"; this.recording = true; this.pressedKeys.clear(); this.comboKeys.clear(); this.button.textContent = localizationProvider.get("PressTheKeyCombination"); document.addEventListener("keydown", this.keydownHandle); document.addEventListener("keyup", this.keyupOrBlurHandle); globalThis.addEventListener("blur", this.blurHandle); }; createElements() { const container = UI.createEl("vot-block", ["vot-hotkey"]); const label = UI.createEl("vot-block", ["vot-hotkey-label"]); label.textContent = this._labelHtml; const button = UI.createEl("vot-block", ["vot-hotkey-button"]); UI.makeButtonLike(button); button.textContent = this.keyText; button.addEventListener("click", this.buttonClickHandle); container.append(label, button); return { container, button, label }; } get key() { return this._key; } get keyText() { if (!this._key) return localizationProvider.get("None"); return formatKeysComboDisplay(this._key); } /** * If you set a different new value, it will trigger the change event */ set key(newKey) { if (this._key === newKey) return; this._key = newKey; this.button.textContent = this.keyText; this.dispatch("change", this._key); } }; /** * Formats a set of key codes into a string representing a key combination */ function formatKeysCombo(keys) { return (Array.isArray(keys) ? keys : Array.from(keys)).map((code) => code.replace("Key", "").replace("Digit", "")).join("+"); } /** * Human-friendly formatting for hotkeys. Does not change stored semantics. */ function formatKeysComboDisplay(keys) { let parts; if (typeof keys === "string") parts = keys.split("+").filter(Boolean); else if (Array.isArray(keys)) parts = keys; else parts = Array.from(keys); const map = (k) => { switch (k) { case "ControlLeft": case "ControlRight": case "Control": return "Ctrl"; case "ShiftLeft": case "ShiftRight": case "Shift": return "Shift"; case "AltLeft": case "AltRight": case "Alt": return "Alt"; case "MetaLeft": case "MetaRight": case "Meta": return "Meta"; case "Space": return "Space"; case "ArrowUp": return "↑"; case "ArrowDown": return "↓"; case "ArrowLeft": return "←"; case "ArrowRight": return "→"; default: return k.replace("Key", "").replace("Digit", ""); } }; const priority = (k) => { const m = map(k); if (m === "Ctrl") return 0; if (m === "Alt") return 1; if (m === "Shift") return 2; if (m === "Meta") return 3; return 10; }; return parts.slice().sort((a, b) => priority(a) - priority(b)).map(map).join("+"); } //#endregion //#region src/ui/views/settings.ts var SETTINGS_EVENT_KEYS = [ "click:bugReport", "click:resetSettings", "update:account", "change:autoTranslate", "change:autoSubtitles", "change:showVideoVolume", "change:audioBooster", "change:syncVolume", "change:subtitlesHighlightWords", "change:subtitlesSmartLayout", "select:responseLanguageSubtitles", "select:subtitlesFontFamily", "change:proxyWorkerHost", "change:useNewAudioPlayer", "change:onlyBypassMediaCSP", "change:showPiPButton", "input:subtitlesMaxLength", "input:subtitlesFontSize", "input:subtitlesBackgroundOpacity", "input:autoHideButtonDelay", "select:proxyTranslationStatus", "select:translationTextService", "select:buttonPosition", "select:menuLanguage" ]; function createSettingsEvents() { const events = {}; for (const key of SETTINGS_EVENT_KEYS) events[key] = new EventImpl(); return events; } var GOOGLE_FONTS_SEARCH_LIMIT = 30; var LANG_PREFIX = "langs."; var [AUTO_SUBTITLE_LANGUAGE_VALUE$1, ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1] = subtitleResponseLanguageModes; var subtitleFontFamilyLabels = { "default-sans": "Default Sans", arial: "Arial", helvetica: "Helvetica", roboto: "Roboto", verdana: "Verdana", "open-sans": "Open Sans", poppins: "Poppins", lato: "Lato", montserrat: "Montserrat", barlow: "Barlow" }; function getSubtitleFontFamilyLabel(fontFamily) { if (isBuiltInSubtitleFontFamily(fontFamily)) return subtitleFontFamilyLabels[fontFamily]; return getGoogleSubtitleFontFamilyName(fontFamily) ?? "Default Sans"; } function getAvailableSubtitleLanguages() { return Object.keys(localizationProvider.defaultLocale).filter((key) => key.startsWith(LANG_PREFIX) && key !== `${LANG_PREFIX}auto`).map((key) => key.slice(6)).sort((left, right) => localizationProvider.getLangLabel(left).localeCompare(localizationProvider.getLangLabel(right))); } function getSubtitleLanguageSettingLabel(value) { if (value === ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1) return localizationProvider.get("VOTOriginalVideoLanguage"); return localizationProvider.getLangLabel(value); } function buildSubtitleLanguageSettingItems(selectedValue) { return [ { label: getSubtitleLanguageSettingLabel(AUTO_SUBTITLE_LANGUAGE_VALUE$1), value: AUTO_SUBTITLE_LANGUAGE_VALUE$1, selected: selectedValue === AUTO_SUBTITLE_LANGUAGE_VALUE$1 }, { label: getSubtitleLanguageSettingLabel(ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1), value: ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1, selected: selectedValue === ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1 }, ...getAvailableSubtitleLanguages().map((language) => ({ label: getSubtitleLanguageSettingLabel(language), value: language, selected: selectedValue === language })) ]; } var SettingsView = class SettingsView { static PERSIST_DELAY_MS = 250; globalPortal; initialized = false; data; videoHandler; suppressSubtitlesSmartLayoutCheckboxChange = false; events = createSettingsEvents(); persistTimerIds = {}; onAuthRefreshMessage = (event) => { if (!isAuthRefreshMessage(event.data)) return; this.refreshAccountFromStorage(); }; dialog; accountButton; accountButtonRefreshTooltip; accountButtonTokenTooltip; accountStorageListenerCleanup; autoTranslateCheckbox; autoSubtitlesCheckbox; dontTranslateLanguagesCheckbox; dontTranslateLanguagesSelect; autoSetVolumeSliderLabel; autoSetVolumeCheckbox; smartDuckingCheckbox; autoSetVolumeSlider; showVideoVolumeSliderCheckbox; audioBoosterCheckbox; audioBoosterTooltip; syncVolumeCheckbox; downloadWithNameCheckbox; sendNotifyOnCompleteCheckbox; useAudioDownloadCheckbox; useAudioDownloadCheckboxLabel; useAudioDownloadCheckboxTooltip; responseLanguageSubtitlesSelectLabel; responseLanguageSubtitlesSelect; subtitlesDownloadFormatSelectLabel; subtitlesDownloadFormatSelect; subtitlesHighlightWordsCheckbox; subtitlesSmartLayoutCheckbox; subtitlesMaxLengthSliderLabel; subtitlesMaxLengthSlider; subtitlesFontSizeSliderLabel; subtitlesFontSizeSlider; subtitlesFontFamilySelectLabel; subtitlesFontFamilySelect; subtitlesBackgroundOpacitySliderLabel; subtitlesBackgroundOpacitySlider; translateHotkeyButton; subtitlesHotkeyButton; proxyWorkerHostTextfield; proxyTranslationStatusSelectLabel; proxyTranslationStatusSelectTooltip; proxyTranslationStatusSelect; translateAPIErrorsCheckbox; useNewAudioPlayerCheckbox; useNewAudioPlayerTooltip; onlyBypassMediaCSPCheckbox; onlyBypassMediaCSPTooltip; translationTextServiceLabel; translationTextServiceSelect; translationTextServiceTooltip; detectServiceLabel; detectServiceSelect; showPiPButtonCheckbox; autoHideButtonDelaySliderLabel; autoHideButtonDelaySlider; buttonPositionSelectLabel; buttonPositionSelect; buttonPositionTooltip; menuLanguageSelectLabel; menuLanguageSelect; bugReportButton; resetSettingsButton; constructor({ globalPortal, data = {}, videoHandler }) { this.globalPortal = globalPortal; this.data = data; this.videoHandler = videoHandler; } isInitialized() { return this.initialized; } createAccordionSection(title, options = {}) { const section = UI.createEl("vot-block", ["vot-settings-section"]); const header = new Details({ titleHtml: title }); header.container.classList.add("vot-settings-section-header"); const sectionId = createDomId("vot-settings-section"); const headerId = `${sectionId}-header`; const contentId = `${sectionId}-content`; header.container.id = headerId; const content = UI.createEl("vot-block", ["vot-settings-section-content"]); content.id = contentId; content.setAttribute("role", "region"); content.setAttribute("aria-labelledby", headerId); header.container.setAttribute("aria-controls", contentId); const setOpen = (open) => { header.container.dataset.open = open ? "true" : "false"; header.container.setAttribute("aria-expanded", open ? "true" : "false"); content.hidden = !open; }; const getOpen = () => header.container.dataset.open === "true"; setOpen(!!options.open); header.addEventListener("click", () => { const isOpen = header.container.dataset.open === "true"; setOpen(!isOpen); }); section.append(header.container, content); return { title, container: section, header: header.container, content, setOpen, getOpen }; } setSubtitlesSmartLayout(checked) { this.data.subtitlesSmartLayout = checked; votStorage.set("subtitlesSmartLayout", checked); debug.log("subtitlesSmartLayout value changed. New value:", checked); if (this.subtitlesSmartLayoutCheckbox?.checked !== checked) { this.suppressSubtitlesSmartLayoutCheckboxChange = true; this.subtitlesSmartLayoutCheckbox.checked = checked; this.suppressSubtitlesSmartLayoutCheckboxChange = false; } this.events["change:subtitlesSmartLayout"].dispatch(checked); } scheduleStoragePersist(key, value) { const prevTimerId = this.persistTimerIds[key]; if (prevTimerId !== void 0) globalThis.clearTimeout(prevTimerId); this.persistTimerIds[key] = globalThis.setTimeout(() => { this.persistTimerIds[key] = void 0; votStorage.set(key, value); }, SettingsView.PERSIST_DELAY_MS); } flushStoragePersists() { for (const key of Object.keys(this.persistTimerIds)) { const timerId = this.persistTimerIds[key]; if (timerId === void 0) continue; globalThis.clearTimeout(timerId); this.persistTimerIds[key] = void 0; const value = this.data[key]; if (typeof value === "number") votStorage.set(key, value); } } bindPersistedSetting({ control, event, apply, storageKey, readPersistedValue, logLabel, dispatch, afterPersist }) { control.addEventListener(event, async (value) => { apply(value); await votStorage.set(storageKey, readPersistedValue()); debug.log(`${logLabel} value changed. New value:`, value); if (afterPersist) await afterPersist(value); dispatch?.(value); }); } bindBufferedNumericSetting({ control, label, storageKey, logLabel, toStoredValue = (value) => value, beforeApply, dispatch }) { control.addEventListener("input", (value) => { label.value = value; beforeApply?.(); const storedValue = toStoredValue(value); this.data[storageKey] = storedValue; this.scheduleStoragePersist(storageKey, storedValue); debug.log(`${logLabel} value changed. New value:`, storedValue); dispatch?.(value); }); } createSettingsSections() { const sections = [ this.createAccordionSection(localizationProvider.get("VOTMyAccount"), { open: true }), this.createAccordionSection(localizationProvider.get("translationSettings"), { open: true }), this.createAccordionSection(localizationProvider.get("subtitlesSettings")), this.createAccordionSection(localizationProvider.get("hotkeysSettings")), this.createAccordionSection(localizationProvider.get("proxySettings")), this.createAccordionSection(localizationProvider.get("miscSettings")), this.createAccordionSection(localizationProvider.get("appearance")), this.createAccordionSection(localizationProvider.get("aboutExtension")) ]; return { accountSection: sections[0], translationSection: sections[1], subtitlesSection: sections[2], hotkeysSection: sections[3], proxySection: sections[4], miscSection: sections[5], appearanceSection: sections[6], aboutSection: sections[7], sections }; } initAccountControls() { this.accountButton = new AccountButton({ avatarId: this.data.account?.avatarId, username: this.data.account?.username, loggedIn: !!this.data.account?.token }); if (votStorage.isSupportOnlyLS) { this.accountButton.refreshButton.setAttribute("disabled", "true"); this.accountButton.actionButton.setAttribute("disabled", "true"); } else this.accountButtonRefreshTooltip = new Tooltip({ target: this.accountButton.refreshButton, content: localizationProvider.get("VOTRefresh"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); this.accountButtonTokenTooltip = new Tooltip({ target: this.accountButton.tokenButton, content: localizationProvider.get("VOTLoginViaToken"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); } bindAccountStorageListener() { this.accountStorageListenerCleanup?.(); this.accountStorageListenerCleanup = votStorage.addValueChangeListener("account", (_key, _oldValue, account) => { this.data.account = account ?? {}; if (!this.isInitialized() || !this.accountButton) return; this.updateAccountInfo(); }); } async refreshAccountFromStorage() { if (votStorage.isSupportOnlyLS) return; this.data.account = await votStorage.get("account", {}); if (!this.isInitialized() || !this.accountButton) return; this.updateAccountInfo(); } buildSubtitleFontItems(selectedFontFamily, dynamicFontFamilies = []) { const items = subtitleFontFamilies.map((fontFamily) => ({ label: subtitleFontFamilyLabels[fontFamily], value: fontFamily, selected: fontFamily === selectedFontFamily })); const dynamicItems = dynamicFontFamilies.filter((familyName) => { const lowerFamilyName = familyName.toLowerCase(); return !items.some((item) => item.label.toLowerCase() === lowerFamilyName); }).map((familyName) => { const fontValue = toGoogleSubtitleFontFamily(familyName); return { label: familyName, value: fontValue, selected: fontValue === selectedFontFamily }; }); if (!isBuiltInSubtitleFontFamily(selectedFontFamily) && !dynamicItems.some((item) => item.value === selectedFontFamily)) { const currentGoogleFontFamily = getGoogleSubtitleFontFamilyName(selectedFontFamily); if (currentGoogleFontFamily) dynamicItems.unshift({ label: currentGoogleFontFamily, value: selectedFontFamily, selected: true }); } return [...items, ...dynamicItems]; } async searchSubtitleFontItems(query, fallbackFontFamily) { const activeFontFamily = Array.from(this.subtitlesFontFamilySelect?.selectedValues ?? [])[0] ?? fallbackFontFamily; const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return this.buildSubtitleFontItems(activeFontFamily); const matchingGoogleFonts = (await loadGoogleFontsCatalog()).filter((familyName) => familyName.toLowerCase().includes(normalizedQuery)).slice(0, GOOGLE_FONTS_SEARCH_LIMIT); return this.buildSubtitleFontItems(activeFontFamily, matchingGoogleFonts); } buildAboutSectionContent(aboutSection) { const envInfo = getEnvironmentInfo(); const safeGMInfo = typeof GM_info === "undefined" ? void 0 : GM_info; const versionInfo = UI.createInformation(`${localizationProvider.get("VOTVersion")}:`, envInfo.scriptVersion === "unknown" ? safeGMInfo?.script?.version || localizationProvider.get("notFound") : envInfo.scriptVersion); const buildAuthors = String("Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng"); const authorsInfo = UI.createInformation(`${localizationProvider.get("VOTAuthors")}:`, (safeGMInfo?.script)?.author || buildAuthors || localizationProvider.get("notFound")); const loaderInfo = UI.createInformation(`${localizationProvider.get("VOTLoader")}:`, envInfo.loader); const userBrowserInfo = UI.createInformation(`${localizationProvider.get("VOTBrowser")}:`, `${envInfo.browser} (${envInfo.os})`); const localeUpdatedAt = (/* @__PURE__ */ new Date((this.data.localeUpdatedAt ?? 0) * 1e3)).toLocaleString(); const localeInfoValue = b`${this.data.localeHash ?? localizationProvider.get("notFound")}
(${localizationProvider.get("VOTUpdatedAt")} ${localeUpdatedAt})`; const localeInfo = UI.createInformation(`${localizationProvider.get("VOTLocaleHash")}:`, localeInfoValue); const updateLocaleFilesButton = UI.createOutlinedButton(localizationProvider.get("VOTUpdateLocaleFiles")); updateLocaleFilesButton.addEventListener("click", async () => { await votStorage.set("localeHash", ""); await localizationProvider.update(true); globalThis.location.reload(); }); aboutSection.content.append(versionInfo.container, authorsInfo.container, loaderInfo.container, userBrowserInfo.container, localeInfo.container, updateLocaleFilesButton); } initUI() { if (this.isInitialized()) throw new Error("[VOT] SettingsView is already initialized"); this.dialog = new Dialog({ titleHtml: localizationProvider.get("VOTSettings") }); this.globalPortal.appendChild(this.dialog.container); const { accountSection, translationSection, subtitlesSection, hotkeysSection, proxySection, miscSection, appearanceSection, aboutSection, sections } = this.createSettingsSections(); this.dialog.bodyContainer.append(...sections.map((section) => section.container)); this.initAccountControls(); this.autoTranslateCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTAutoTranslate"), checked: this.data.autoTranslate }); this.autoSubtitlesCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTAutoSubtitles"), checked: this.data.autoSubtitles }); const dontTranslateLanguages = this.data.dontTranslateLanguages ?? []; this.dontTranslateLanguagesCheckbox = new Checkbox({ labelHtml: localizationProvider.get("DontTranslateSelectedLanguages"), checked: this.data.enabledDontTranslateLanguages }); this.dontTranslateLanguagesSelect = new Select({ dialogParent: this.globalPortal, dialogTitle: localizationProvider.get("DontTranslateSelectedLanguages"), selectTitle: dontTranslateLanguages.map((lang) => localizationProvider.get(`langs.${lang}`)).join(", ") || localizationProvider.get("DontTranslateSelectedLanguages"), items: Select.genLanguageItems(availableLangs).map((item) => ({ ...item, selected: dontTranslateLanguages.includes(item.value) })), multiSelect: true, labelElement: this.dontTranslateLanguagesCheckbox.container }); this.dontTranslateLanguagesSelect.disabled = !this.dontTranslateLanguagesCheckbox.checked; const autoVolume = this.data.autoVolume ?? 15; this.autoSetVolumeSliderLabel = new SliderLabel({ labelText: localizationProvider.get("VOTAutoSetVolume"), value: autoVolume }); this.autoSetVolumeCheckbox = new Checkbox({ labelHtml: this.autoSetVolumeSliderLabel.container, checked: this.data.enabledAutoVolume ?? true }); this.autoSetVolumeSlider = new Slider({ labelHtml: this.autoSetVolumeCheckbox.container, value: autoVolume, min: 0 }); const syncVolumeEnabled = Boolean(this.data.syncVolume); this.autoSetVolumeSlider.disabled = !this.autoSetVolumeCheckbox.checked; this.smartDuckingCheckbox = new Checkbox({ labelHtml: localizationProvider.get("smartDucking"), checked: this.data.enabledSmartDucking ?? true }); this.smartDuckingCheckbox.disabled = syncVolumeEnabled || !this.autoSetVolumeCheckbox.checked; this.showVideoVolumeSliderCheckbox = new Checkbox({ labelHtml: localizationProvider.get("showVideoVolumeSlider"), checked: this.data.showVideoSlider }); this.audioBoosterCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTAudioBooster"), checked: this.data.audioBooster }); if (!this.videoHandler?.isAudioContextSupported) { this.audioBoosterCheckbox.disabled = true; this.audioBoosterTooltip = new Tooltip({ target: this.audioBoosterCheckbox.container, content: localizationProvider.get("VOTNeedWebAudioAPI"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); } this.syncVolumeCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTSyncVolume"), checked: this.data.syncVolume }); this.downloadWithNameCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTDownloadWithName"), checked: this.data.downloadWithName }); this.downloadWithNameCheckbox.disabled = !isSupportGMXhr; this.sendNotifyOnCompleteCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTSendNotifyOnComplete"), checked: this.data.sendNotifyOnComplete }); this.useAudioDownloadCheckboxLabel = new Label({ labelText: localizationProvider.get("VOTUseAudioDownload"), icon: WARNING_ICON }); this.useAudioDownloadCheckbox = new Checkbox({ labelHtml: this.useAudioDownloadCheckboxLabel.container, checked: this.data.useAudioDownload }); if (!isSupportGMXhr) this.useAudioDownloadCheckbox.disabled = true; this.useAudioDownloadCheckboxTooltip = new Tooltip({ target: this.useAudioDownloadCheckboxLabel.container, content: localizationProvider.get("VOTUseAudioDownloadWarning"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); accountSection.content.append(this.accountButton.container); translationSection.content.append(this.autoTranslateCheckbox.container, this.autoSubtitlesCheckbox.container, this.dontTranslateLanguagesSelect.container, this.autoSetVolumeSlider.container, this.smartDuckingCheckbox.container, this.showVideoVolumeSliderCheckbox.container, this.audioBoosterCheckbox.container, this.syncVolumeCheckbox.container, this.downloadWithNameCheckbox.container, this.sendNotifyOnCompleteCheckbox.container, this.useAudioDownloadCheckbox.container); this.subtitlesDownloadFormatSelectLabel = new Label({ labelText: localizationProvider.get("VOTSubtitlesDownloadFormat") }); this.subtitlesDownloadFormatSelect = new Select({ selectTitle: this.data.subtitlesDownloadFormat ?? localizationProvider.get("VOTSubtitlesDownloadFormat"), dialogTitle: localizationProvider.get("VOTSubtitlesDownloadFormat"), dialogParent: this.globalPortal, labelElement: this.subtitlesDownloadFormatSelectLabel.container, items: subtitleFormats.map((format) => ({ label: format.toUpperCase(), value: format, selected: format === this.data.subtitlesDownloadFormat })) }); const responseLanguageSubtitles = this.data.responseLanguageSubtitles ?? AUTO_SUBTITLE_LANGUAGE_VALUE$1; this.responseLanguageSubtitlesSelectLabel = new Label({ labelText: localizationProvider.get("VOTDefaultSubtitlesLanguage") }); this.responseLanguageSubtitlesSelect = new Select({ selectTitle: getSubtitleLanguageSettingLabel(responseLanguageSubtitles), dialogTitle: localizationProvider.get("VOTDefaultSubtitlesLanguage"), dialogParent: this.globalPortal, labelElement: this.responseLanguageSubtitlesSelectLabel.container, items: buildSubtitleLanguageSettingItems(responseLanguageSubtitles) }); this.subtitlesHighlightWordsCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTHighlightWords"), checked: this.data.highlightWords }); const subtitlesSmartLayout = this.data.subtitlesSmartLayout ?? true; this.subtitlesSmartLayoutCheckbox = new Checkbox({ labelHtml: localizationProvider.get("subtitlesSmartLayout"), checked: subtitlesSmartLayout }); const subtitlesMaxLength = this.data.subtitlesMaxLength ?? 300; this.subtitlesMaxLengthSliderLabel = new SliderLabel({ labelText: localizationProvider.get("VOTSubtitlesMaxLength"), labelEOL: ":", value: subtitlesMaxLength, symbol: "" }); this.subtitlesMaxLengthSlider = new Slider({ labelHtml: this.subtitlesMaxLengthSliderLabel.container, value: subtitlesMaxLength, min: 50, max: 300 }); const subtitlesFontSize = this.data.subtitlesFontSize ?? 20; this.subtitlesFontSizeSliderLabel = new SliderLabel({ labelText: localizationProvider.get("VOTSubtitlesFontSize"), labelEOL: ":", value: subtitlesFontSize, symbol: "px" }); this.subtitlesFontSizeSlider = new Slider({ labelHtml: this.subtitlesFontSizeSliderLabel.container, value: subtitlesFontSize, min: 8, max: 50 }); const storedSubtitlesFontFamily = typeof this.data.subtitlesFontFamily === "string" ? this.data.subtitlesFontFamily : void 0; const subtitlesFontFamily = storedSubtitlesFontFamily && (isBuiltInSubtitleFontFamily(storedSubtitlesFontFamily) || getGoogleSubtitleFontFamilyName(storedSubtitlesFontFamily)) ? storedSubtitlesFontFamily : "default-sans"; this.subtitlesFontFamilySelectLabel = new Label({ labelText: localizationProvider.get("VOTSubtitlesFont") }); this.subtitlesFontFamilySelect = new Select({ selectTitle: getSubtitleFontFamilyLabel(subtitlesFontFamily), dialogTitle: localizationProvider.get("VOTSubtitlesFont"), dialogParent: this.globalPortal, labelElement: this.subtitlesFontFamilySelectLabel.container, items: this.buildSubtitleFontItems(subtitlesFontFamily), searchItemsProvider: (query) => this.searchSubtitleFontItems(query, subtitlesFontFamily) }); this.subtitlesFontFamilySelect.addEventListener("selectItem", (item) => { if (!this.subtitlesFontFamilySelect) return; this.subtitlesFontFamilySelect.updateItems(this.buildSubtitleFontItems(item)); this.subtitlesFontFamilySelect.selectTitle = getSubtitleFontFamilyLabel(item); }); const subtitlesOpacity = this.data.subtitlesOpacity ?? 20; this.subtitlesBackgroundOpacitySliderLabel = new SliderLabel({ labelText: localizationProvider.get("VOTSubtitlesOpacity"), labelEOL: ":", value: subtitlesOpacity, symbol: "%" }); this.subtitlesBackgroundOpacitySlider = new Slider({ labelHtml: this.subtitlesBackgroundOpacitySliderLabel.container, value: subtitlesOpacity, min: 0, max: 100 }); subtitlesSection.content.append(this.responseLanguageSubtitlesSelect.container, this.subtitlesDownloadFormatSelect.container, this.subtitlesFontFamilySelect.container, this.subtitlesHighlightWordsCheckbox.container, this.subtitlesSmartLayoutCheckbox.container, this.subtitlesMaxLengthSlider.container, this.subtitlesFontSizeSlider.container, this.subtitlesBackgroundOpacitySlider.container); this.translateHotkeyButton = new HotkeyButton({ labelHtml: localizationProvider.get("translateVideo"), key: this.data.translationHotkey }); this.subtitlesHotkeyButton = new HotkeyButton({ labelHtml: localizationProvider.get("VOTSubtitles"), key: this.data.subtitlesHotkey }); hotkeysSection.content.append(this.translateHotkeyButton.container, this.subtitlesHotkeyButton.container); this.proxyWorkerHostTextfield = new Textfield({ labelHtml: localizationProvider.get("VOTProxyWorkerHost"), value: this.data.proxyWorkerHost, placeholder: proxyWorkerHost }); const proxyEnabledLabels = [ localizationProvider.get("VOTTranslateProxyDisabled"), localizationProvider.get("VOTTranslateProxyEnabled"), localizationProvider.get("VOTTranslateProxyEverything") ]; const translateProxyEnabled = this.data.translateProxyEnabled ?? 0; const countryCode = getCountryCode(); const isTranslateProxyRequired = countryCode !== null && proxyOnlyCountries.includes(countryCode); this.proxyTranslationStatusSelectLabel = new Label({ icon: isTranslateProxyRequired ? WARNING_ICON : void 0, labelText: localizationProvider.get("VOTTranslateProxyStatus") }); if (isTranslateProxyRequired) this.proxyTranslationStatusSelectTooltip = new Tooltip({ target: this.proxyTranslationStatusSelectLabel.icon, content: localizationProvider.get("VOTTranslateProxyStatusDefault"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); this.proxyTranslationStatusSelect = new Select({ selectTitle: proxyEnabledLabels[translateProxyEnabled], dialogTitle: localizationProvider.get("VOTTranslateProxyStatus"), dialogParent: this.globalPortal, labelElement: this.proxyTranslationStatusSelectLabel.container, items: proxyEnabledLabels.map((label, idx) => ({ label, value: idx.toString(), selected: idx === translateProxyEnabled, disabled: idx === 0 && isProxyOnlyExtension })) }); proxySection.content.append(this.proxyWorkerHostTextfield.container, this.proxyTranslationStatusSelect.container); this.translateAPIErrorsCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTTranslateAPIErrors"), checked: this.data.translateAPIErrors ?? true }); this.translateAPIErrorsCheckbox.hidden = localizationProvider.lang === "ru"; this.useNewAudioPlayerCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTNewAudioPlayer"), checked: this.data.newAudioPlayer }); if (!this.videoHandler?.isAudioContextSupported) { this.useNewAudioPlayerCheckbox.disabled = true; this.useNewAudioPlayerTooltip = new Tooltip({ target: this.useNewAudioPlayerCheckbox.container, content: localizationProvider.get("VOTNeedWebAudioAPI"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); } const onlyBypassMediaCSPLabel = this.videoHandler?.site.needBypassCSP ? `${localizationProvider.get("VOTOnlyBypassMediaCSP")} (${localizationProvider.get("VOTMediaCSPEnabledOnSite")})` : localizationProvider.get("VOTOnlyBypassMediaCSP"); this.onlyBypassMediaCSPCheckbox = new Checkbox({ labelHtml: onlyBypassMediaCSPLabel, checked: this.data.onlyBypassMediaCSP, isSubCheckbox: true }); if (!this.videoHandler?.isAudioContextSupported) this.onlyBypassMediaCSPTooltip = new Tooltip({ target: this.onlyBypassMediaCSPCheckbox.container, content: localizationProvider.get("VOTNeedWebAudioAPI"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); this.onlyBypassMediaCSPCheckbox.disabled = !this.data.newAudioPlayer && !!this.videoHandler?.isAudioContextSupported; if (!this.data.newAudioPlayer) this.onlyBypassMediaCSPCheckbox.hidden = true; this.translationTextServiceLabel = new Label({ labelText: localizationProvider.get("VOTTranslationTextService"), icon: HELP_ICON }); const translationService = this.data.translationService ?? "yandexbrowser"; this.translationTextServiceSelect = new Select({ selectTitle: localizationProvider.get(`services.${translationService}`), dialogTitle: localizationProvider.get("VOTTranslationTextService"), dialogParent: this.globalPortal, labelElement: this.translationTextServiceLabel.container, items: foswlyServices.map((service) => ({ label: localizationProvider.get(`services.${service}`), value: service, selected: service === translationService })) }); this.translationTextServiceTooltip = new Tooltip({ target: this.translationTextServiceLabel.icon, content: localizationProvider.get("VOTNotAffectToVoice"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); this.detectServiceLabel = new Label({ labelText: localizationProvider.get("VOTDetectService") }); const detectService = this.data.detectService ?? "yandexbrowser"; this.detectServiceSelect = new Select({ selectTitle: localizationProvider.get(`services.${detectService}`), dialogTitle: localizationProvider.get("VOTDetectService"), dialogParent: this.globalPortal, labelElement: this.detectServiceLabel.container, items: detectServices.map((service) => ({ label: localizationProvider.get(`services.${service}`), value: service, selected: service === detectService })) }); this.showPiPButtonCheckbox = new Checkbox({ labelHtml: localizationProvider.get("VOTShowPiPButton"), checked: this.data.showPiPButton }); this.showPiPButtonCheckbox.hidden = !isPiPAvailable(); const autoHideButtonDelaySec = Math.round((this.data.autoHideButtonDelay ?? 1e3) / 1e3 * 10) / 10; this.autoHideButtonDelaySliderLabel = new SliderLabel({ labelText: localizationProvider.get("autoHideButtonDelay"), labelEOL: ":", value: autoHideButtonDelaySec, symbol: ` ${localizationProvider.get("secs")}` }); this.autoHideButtonDelaySlider = new Slider({ labelHtml: this.autoHideButtonDelaySliderLabel.container, value: autoHideButtonDelaySec, min: .1, max: 3, step: .1 }); this.buttonPositionSelectLabel = new Label({ labelText: localizationProvider.get("buttonPosition"), icon: HELP_ICON }); const buttonPos = normalizeButtonPosition(this.data.buttonPos); this.buttonPositionSelect = new Select({ selectTitle: localizationProvider.get(`position.${buttonPos}`), dialogTitle: localizationProvider.get("buttonPosition"), labelElement: this.buttonPositionSelectLabel.container, dialogParent: this.globalPortal, items: positions.map((position) => ({ label: localizationProvider.get(`position.${position}`), value: position, selected: position === buttonPos })) }); this.buttonPositionTooltip = new Tooltip({ target: this.buttonPositionSelectLabel.icon, content: localizationProvider.get("minButtonPositionContainer"), position: "bottom", backgroundColor: "var(--vot-helper-ondialog)", parentElement: this.globalPortal }); this.menuLanguageSelectLabel = new Label({ labelText: localizationProvider.get("VOTMenuLanguage") }); this.menuLanguageSelect = new Select({ selectTitle: localizationProvider.get(`langs.${localizationProvider.langOverride}`), dialogTitle: localizationProvider.get("VOTMenuLanguage"), labelElement: this.menuLanguageSelectLabel.container, dialogParent: this.globalPortal, items: Select.genLanguageItems(localizationProvider.getAvailableLangs(), localizationProvider.langOverride) }); this.bugReportButton = UI.createOutlinedButton(localizationProvider.get("VOTBugReport")); this.resetSettingsButton = UI.createButton(localizationProvider.get("resetSettings")); miscSection.content.append(this.translateAPIErrorsCheckbox.container, this.useNewAudioPlayerCheckbox.container, this.onlyBypassMediaCSPCheckbox.container); translationSection.content.append(this.translationTextServiceSelect.container, this.detectServiceSelect.container); appearanceSection.content.append(this.showPiPButtonCheckbox.container, this.autoHideButtonDelaySlider.container, this.buttonPositionSelect.container, this.menuLanguageSelect.container); this.buildAboutSectionContent(aboutSection); this.dialog.footerContainer.append(this.bugReportButton, this.resetSettingsButton); this.initialized = true; return this; } initUIEvents() { if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized"); globalThis.addEventListener("message", this.onAuthRefreshMessage); this.accountButton.addEventListener("click", async () => { if (votStorage.isSupportOnlyLS) return; if (this.accountButton.loggedIn) { await votStorage.delete("account"); this.data.account = {}; this.updateAccountInfo(); return; } openAuthWindow(); }); this.accountButton.addEventListener("click:secret", async () => { const dialog = new Dialog({ titleHtml: localizationProvider.get("VOTLoginViaToken"), isTemp: true }); this.globalPortal.appendChild(dialog.container); const tokenInfoEl = UI.createEl("vot-block", void 0, localizationProvider.get("VOTYandexTokenInfo")); const tokenTextfield = new Textfield({ labelHtml: localizationProvider.get("VOTYandexToken"), value: this.data.account?.token }); tokenTextfield.addEventListener("change", async (token) => { this.data.account = token ? { expires: Date.now() + 3153418e4, token } : {}; await votStorage.set("account", this.data.account); this.updateAccountInfo(); }); dialog.bodyContainer.append(tokenInfoEl, tokenTextfield.container); dialog.open(); }); this.accountButton.addEventListener("refresh", async () => { await this.refreshAccountFromStorage(); }); this.bindAccountStorageListener(); this.bindPersistedSetting({ control: this.autoTranslateCheckbox, event: "change", apply: (checked) => { this.data.autoTranslate = checked; }, storageKey: "autoTranslate", readPersistedValue: () => this.data.autoTranslate, logLabel: "autoTranslate", dispatch: (checked) => this.events["change:autoTranslate"].dispatch(checked) }); this.bindPersistedSetting({ control: this.autoSubtitlesCheckbox, event: "change", apply: (checked) => { this.data.autoSubtitles = checked; }, storageKey: "autoSubtitles", readPersistedValue: () => this.data.autoSubtitles, logLabel: "autoSubtitles", dispatch: (checked) => this.events["change:autoSubtitles"].dispatch(checked) }); this.dontTranslateLanguagesCheckbox.addEventListener("change", async (checked) => { this.data.enabledDontTranslateLanguages = checked; this.dontTranslateLanguagesSelect.disabled = !checked; await votStorage.set("enabledDontTranslateLanguages", this.data.enabledDontTranslateLanguages); debug.log("enabledDontTranslateLanguages value changed. New value:", checked); }); this.dontTranslateLanguagesSelect.addEventListener("selectItem", async (values) => { this.data.dontTranslateLanguages = values; await votStorage.set("dontTranslateLanguages", this.data.dontTranslateLanguages); debug.log("dontTranslateLanguages value changed. New value:", values); }); this.bindPersistedSetting({ control: this.autoSetVolumeCheckbox, event: "change", apply: (checked) => { this.data.enabledAutoVolume = checked; this.autoSetVolumeSlider.disabled = !checked; this.smartDuckingCheckbox.disabled = !checked || Boolean(this.syncVolumeCheckbox?.checked); }, storageKey: "enabledAutoVolume", readPersistedValue: () => this.data.enabledAutoVolume, logLabel: "enabledAutoVolume", afterPersist: async () => this.videoHandler?.setupAudioSettings?.() }); this.bindPersistedSetting({ control: this.smartDuckingCheckbox, event: "change", apply: (checked) => { this.data.enabledSmartDucking = checked; }, storageKey: "enabledSmartDucking", readPersistedValue: () => this.data.enabledSmartDucking, logLabel: "enabledSmartDucking", afterPersist: async () => this.videoHandler?.setupAudioSettings?.() }); this.bindPersistedSetting({ control: this.autoSetVolumeSlider, event: "input", apply: (value) => { this.data.autoVolume = this.autoSetVolumeSliderLabel.value = value; }, storageKey: "autoVolume", readPersistedValue: () => this.data.autoVolume, logLabel: "autoVolume" }); this.bindPersistedSetting({ control: this.showVideoVolumeSliderCheckbox, event: "change", apply: (checked) => { this.data.showVideoSlider = checked; }, storageKey: "showVideoSlider", readPersistedValue: () => this.data.showVideoSlider, logLabel: "showVideoVolumeSlider", dispatch: (checked) => this.events["change:showVideoVolume"].dispatch(checked) }); this.bindPersistedSetting({ control: this.audioBoosterCheckbox, event: "change", apply: (checked) => { this.data.audioBooster = checked; }, storageKey: "audioBooster", readPersistedValue: () => this.data.audioBooster, logLabel: "audioBooster", dispatch: (checked) => this.events["change:audioBooster"].dispatch(checked) }); this.bindPersistedSetting({ control: this.syncVolumeCheckbox, event: "change", apply: (checked) => { this.data.syncVolume = checked; this.autoSetVolumeSlider.disabled = !this.autoSetVolumeCheckbox?.checked; this.smartDuckingCheckbox.disabled = checked || !this.autoSetVolumeCheckbox?.checked; if (checked && this.smartDuckingCheckbox?.checked) this.smartDuckingCheckbox.checked = false; }, storageKey: "syncVolume", readPersistedValue: () => this.data.syncVolume, logLabel: "syncVolume", dispatch: (checked) => this.events["change:syncVolume"].dispatch(checked) }); this.bindPersistedSetting({ control: this.downloadWithNameCheckbox, event: "change", apply: (checked) => { this.data.downloadWithName = checked; }, storageKey: "downloadWithName", readPersistedValue: () => this.data.downloadWithName, logLabel: "downloadWithName" }); this.bindPersistedSetting({ control: this.sendNotifyOnCompleteCheckbox, event: "change", apply: (checked) => { this.data.sendNotifyOnComplete = checked; }, storageKey: "sendNotifyOnComplete", readPersistedValue: () => this.data.sendNotifyOnComplete, logLabel: "sendNotifyOnComplete" }); this.bindPersistedSetting({ control: this.useAudioDownloadCheckbox, event: "change", apply: (checked) => { this.data.useAudioDownload = checked; }, storageKey: "useAudioDownload", readPersistedValue: () => this.data.useAudioDownload, logLabel: "useAudioDownload" }); this.bindPersistedSetting({ control: this.responseLanguageSubtitlesSelect, event: "selectItem", apply: (item) => { this.data.responseLanguageSubtitles = item; this.responseLanguageSubtitlesSelect?.updateItems(buildSubtitleLanguageSettingItems(item)); if (this.responseLanguageSubtitlesSelect) this.responseLanguageSubtitlesSelect.selectTitle = getSubtitleLanguageSettingLabel(item); }, storageKey: "responseLanguageSubtitles", readPersistedValue: () => this.data.responseLanguageSubtitles, logLabel: "responseLanguageSubtitles", dispatch: (item) => this.events["select:responseLanguageSubtitles"].dispatch(item) }); this.bindPersistedSetting({ control: this.subtitlesDownloadFormatSelect, event: "selectItem", apply: (item) => { this.data.subtitlesDownloadFormat = item; }, storageKey: "subtitlesDownloadFormat", readPersistedValue: () => this.data.subtitlesDownloadFormat, logLabel: "subtitlesDownloadFormat" }); this.bindPersistedSetting({ control: this.subtitlesHighlightWordsCheckbox, event: "change", apply: (checked) => { this.data.highlightWords = checked; }, storageKey: "highlightWords", readPersistedValue: () => this.data.highlightWords, logLabel: "highlightWords", dispatch: (checked) => this.events["change:subtitlesHighlightWords"].dispatch(checked) }); this.subtitlesSmartLayoutCheckbox?.addEventListener("change", (checked) => { if (this.suppressSubtitlesSmartLayoutCheckboxChange) return; this.setSubtitlesSmartLayout(checked); }); const disableSmartLayout = () => { if ((this.data.subtitlesSmartLayout ?? true) === true) this.setSubtitlesSmartLayout(false); }; this.bindBufferedNumericSetting({ control: this.subtitlesMaxLengthSlider, label: this.subtitlesMaxLengthSliderLabel, storageKey: "subtitlesMaxLength", logLabel: "subtitlesMaxLength", beforeApply: disableSmartLayout, dispatch: (value) => this.events["input:subtitlesMaxLength"].dispatch(value) }); this.bindBufferedNumericSetting({ control: this.subtitlesFontSizeSlider, label: this.subtitlesFontSizeSliderLabel, storageKey: "subtitlesFontSize", logLabel: "subtitlesFontSize", beforeApply: disableSmartLayout, dispatch: (value) => this.events["input:subtitlesFontSize"].dispatch(value) }); this.bindBufferedNumericSetting({ control: this.subtitlesBackgroundOpacitySlider, label: this.subtitlesBackgroundOpacitySliderLabel, storageKey: "subtitlesOpacity", logLabel: "subtitlesOpacity", dispatch: (value) => this.events["input:subtitlesBackgroundOpacity"].dispatch(value) }); this.bindPersistedSetting({ control: this.subtitlesFontFamilySelect, event: "selectItem", apply: (item) => { this.data.subtitlesFontFamily = item; }, storageKey: "subtitlesFontFamily", readPersistedValue: () => this.data.subtitlesFontFamily, logLabel: "subtitlesFontFamily", dispatch: (item) => this.events["select:subtitlesFontFamily"].dispatch(item) }); this.bindPersistedSetting({ control: this.translateHotkeyButton, event: "change", apply: (key) => { this.data.translationHotkey = key; }, storageKey: "translationHotkey", readPersistedValue: () => this.data.translationHotkey, logLabel: "translationHotkey" }); this.bindPersistedSetting({ control: this.subtitlesHotkeyButton, event: "change", apply: (key) => { this.data.subtitlesHotkey = key; }, storageKey: "subtitlesHotkey", readPersistedValue: () => this.data.subtitlesHotkey, logLabel: "subtitlesHotkey" }); this.proxyWorkerHostTextfield.addEventListener("change", async (value) => { this.data.proxyWorkerHost = value || "vot-worker.eu.cc"; await votStorage.set("proxyWorkerHost", this.data.proxyWorkerHost); debug.log("proxyWorkerHost value changed. New value:", this.data.proxyWorkerHost); this.events["change:proxyWorkerHost"].dispatch(value); }); this.proxyTranslationStatusSelect.addEventListener("selectItem", async (item) => { this.data.translateProxyEnabled = Number.parseInt(item, 10); await votStorage.set("translateProxyEnabled", this.data.translateProxyEnabled); await votStorage.set("translateProxyEnabledDefault", false); debug.log("translateProxyEnabled value changed. New value:", this.data.translateProxyEnabled); this.events["select:proxyTranslationStatus"].dispatch(item); }); this.bindPersistedSetting({ control: this.translateAPIErrorsCheckbox, event: "change", apply: (checked) => { this.data.translateAPIErrors = checked; }, storageKey: "translateAPIErrors", readPersistedValue: () => this.data.translateAPIErrors, logLabel: "translateAPIErrors" }); this.bindPersistedSetting({ control: this.useNewAudioPlayerCheckbox, event: "change", apply: (checked) => { this.data.newAudioPlayer = checked; this.onlyBypassMediaCSPCheckbox.disabled = this.onlyBypassMediaCSPCheckbox.hidden = !checked; }, storageKey: "newAudioPlayer", readPersistedValue: () => this.data.newAudioPlayer, logLabel: "newAudioPlayer", dispatch: (checked) => this.events["change:useNewAudioPlayer"].dispatch(checked) }); this.bindPersistedSetting({ control: this.onlyBypassMediaCSPCheckbox, event: "change", apply: (checked) => { this.data.onlyBypassMediaCSP = checked; }, storageKey: "onlyBypassMediaCSP", readPersistedValue: () => this.data.onlyBypassMediaCSP, logLabel: "onlyBypassMediaCSP", dispatch: (checked) => this.events["change:onlyBypassMediaCSP"].dispatch(checked) }); this.bindPersistedSetting({ control: this.translationTextServiceSelect, event: "selectItem", apply: (item) => { this.data.translationService = item; }, storageKey: "translationService", readPersistedValue: () => this.data.translationService, logLabel: "translationService", dispatch: (item) => this.events["select:translationTextService"].dispatch(item) }); this.bindPersistedSetting({ control: this.detectServiceSelect, event: "selectItem", apply: (item) => { this.data.detectService = item; }, storageKey: "detectService", readPersistedValue: () => this.data.detectService, logLabel: "detectService" }); this.bindPersistedSetting({ control: this.showPiPButtonCheckbox, event: "change", apply: (checked) => { this.data.showPiPButton = checked; }, storageKey: "showPiPButton", readPersistedValue: () => this.data.showPiPButton, logLabel: "showPiPButton", dispatch: (checked) => this.events["change:showPiPButton"].dispatch(checked) }); this.bindBufferedNumericSetting({ control: this.autoHideButtonDelaySlider, label: this.autoHideButtonDelaySliderLabel, storageKey: "autoHideButtonDelay", logLabel: "autoHideButtonDelay", toStoredValue: (value) => Math.round(value * 1e3), dispatch: (value) => this.events["input:autoHideButtonDelay"].dispatch(value) }); this.bindPersistedSetting({ control: this.buttonPositionSelect, event: "selectItem", apply: (item) => { this.data.buttonPos = item; }, storageKey: "buttonPos", readPersistedValue: () => this.data.buttonPos, logLabel: "buttonPos", dispatch: (item) => this.events["select:buttonPosition"].dispatch(item) }); this.menuLanguageSelect.addEventListener("selectItem", async (item) => { if (!await localizationProvider.changeLang(item)) return; this.data.localeUpdatedAt = await votStorage.get("localeUpdatedAt", 0); this.events["select:menuLanguage"].dispatch(item); }); this.bugReportButton.addEventListener("click", () => this.events["click:bugReport"].dispatch()); this.resetSettingsButton.addEventListener("click", () => this.events["click:resetSettings"].dispatch()); return this; } addEventListener(type, listener) { this.events[type].addListener(listener); return this; } removeEventListener(type, listener) { this.events[type].removeListener(listener); return this; } doReleaseUI() { this.dialog?.remove(); for (const tooltip of [ this.accountButtonRefreshTooltip, this.accountButtonTokenTooltip, this.audioBoosterTooltip, this.useAudioDownloadCheckboxTooltip, this.useNewAudioPlayerTooltip, this.onlyBypassMediaCSPTooltip, this.translationTextServiceTooltip, this.proxyTranslationStatusSelectTooltip, this.buttonPositionTooltip ]) tooltip?.release(); } doReleaseUIEvents() { this.accountStorageListenerCleanup?.(); this.accountStorageListenerCleanup = void 0; globalThis.removeEventListener("message", this.onAuthRefreshMessage); this.flushStoragePersists(); for (const event of Object.values(this.events)) event.clear(); } release() { if (!this.isInitialized()) return this; this.doReleaseUIEvents(); this.doReleaseUI(); this.initialized = false; return this; } updateAccountInfo() { if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized"); const loggedIn = !!this.data.account?.token; this.accountButton.avatarId = this.data.account?.avatarId; this.accountButton.loggedIn = loggedIn; this.accountButton.username = this.data.account?.username; this.events["update:account"].dispatch(this.data.account); return this; } open() { if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized"); return this.dialog.open(); } close() { if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized"); return this.dialog.close(); } }; //#endregion //#region src/ui/manager.ts var UIManager = class { mount; initialized = false; videoHandler; intervalIdleChecker; data; votGlobalPortal; globalPortalMount; /** * Contains all elements over video player e.g. button, menu and etc */ votOverlayView; /** * Dialog settings menu */ votSettingsView; constructor({ mount, data = {}, videoHandler, intervalIdleChecker }) { this.mount = mount; this.videoHandler = videoHandler; this.data = data; this.intervalIdleChecker = intervalIdleChecker; } get root() { return this.mount.root; } get portalContainer() { return this.mount.portalContainer; } getSubtitlesMountContainer() { return this.votOverlayView?.root ?? this.mount.subtitlesMountContainer; } isInitialized() { return this.initialized; } initUI() { if (this.isInitialized()) throw new Error("[VOT] UIManager is already initialized"); this.initialized = true; this.globalPortalMount = createShadowMount({ parent: this.getGlobalPortalHost(this.mount), rootClasses: ["vot-portal"] }); this.votGlobalPortal = this.globalPortalMount.root; this.votOverlayView = new OverlayView({ mount: this.mount, globalPortal: this.votGlobalPortal, data: this.data, videoHandler: this.videoHandler, intervalIdleChecker: this.intervalIdleChecker }); this.votOverlayView.initUI(normalizeButtonPosition(this.data.buttonPos)); this.votSettingsView = new SettingsView({ globalPortal: this.votGlobalPortal, data: this.data, videoHandler: this.videoHandler }); this.votSettingsView.initUI(); this.videoHandler?.subtitlesWidget?.updateMount({ container: this.getSubtitlesMountContainer() }); return this; } updateMount(mount) { reparentShadowMount(this.globalPortalMount, this.getGlobalPortalHost(mount)); this.mount = applyOverlayMountUpdate(this.mount, mount, (nextMount) => { this.votOverlayView?.updateMount(nextMount); }); this.videoHandler?.subtitlesWidget?.updateMount({ container: this.getSubtitlesMountContainer() }); return this; } getGlobalPortalHost(_mount) { const fullscreenInfo = this.videoHandler?.fullscreenHelper?.getFullscreenInfo(); if (fullscreenInfo?.element && fullscreenInfo.belongsToCurrentVideo) return fullscreenInfo.shadowRoot ?? fullscreenInfo.element; return document.documentElement; } initUIEvents() { if (!this.isInitialized()) throw new Error("[VOT] UIManager isn't initialized"); this.votOverlayView.initUIEvents(); this.bindOverlayViewEvents(); this.votSettingsView.initUIEvents(); this.bindSettingsViewEvents(); } bindOverlayViewEvents() { const overlayView = this.votOverlayView; if (!overlayView) return; overlayView.addEventListener("click:translate", async () => { await this.handleTranslationBtnClick(); }).addEventListener("click:pip", async () => { if (!this.videoHandler) return; try { if (document.pictureInPictureElement != null) await document.exitPictureInPicture(); else await this.videoHandler.video.requestPictureInPicture(); } catch (err) { debug.warn("[VOT] Failed to toggle Picture-in-Picture", err); } }).addEventListener("click:subtitles", async () => { if (!this.videoHandler) return; await this.videoHandler.toggleSubtitlesForCurrentLangPair(); }).addEventListener("click:settings", async () => { this.videoHandler?.subtitlesWidget?.releaseTooltip(); this.videoHandler?.overlayVisibility?.cancel(); this.videoHandler?.overlayVisibility?.show(); this.votSettingsView.open(); }).addEventListener("click:downloadTranslation", async () => { await this.handleDownloadTranslationClick(); }).addEventListener("click:downloadSubtitles", async () => { await this.handleDownloadSubtitlesClick(); }).addEventListener("input:videoVolume", (volume) => { if (!this.videoHandler) return; if (volume === 0) this.videoHandler.setVideoMuted(true); else { if (this.videoHandler.isMuted()) this.videoHandler.setVideoMuted(false); const nextVolume01 = volume / 100; this.videoHandler.setVideoVolume(nextVolume01); this.videoHandler.applyManualVideoVolumeOverride(nextVolume01); } if (!this.data.syncVolume) { this.videoHandler.onVideoVolumeSliderSynced(volume); return; } this.videoHandler.syncVolumeWrapper("video", volume); }).addEventListener("input:translationVolume", (volume) => { if (!this.videoHandler) return; const nextVolume = volume ?? this.data.defaultVolume ?? 100; safeSetPlayerVolume(this.videoHandler.audioPlayer.player, nextVolume / 100); if (!this.data.syncVolume) { this.videoHandler.onTranslationVolumeSliderSynced(nextVolume); return; } const syncResult = this.videoHandler.syncVolumeWrapper("translation", nextVolume); if (typeof syncResult?.nextVideo === "number") this.videoHandler.applyManualVideoVolumeOverride(syncResult.nextVideo / 100); }).addEventListener("select:fromLanguage", async () => { if (!this.videoHandler) return; await this.videoHandler.refreshAutoSubtitlesForCurrentLangPair(); }).addEventListener("select:subtitles", (data) => { if (!this.videoHandler) return; this.runDetached(this.videoHandler.changeSubtitlesLang(data), "Failed to change subtitles language"); }).addEventListener("select:voiceType", () => { if (!this.videoHandler) return; const wasActive = this.videoHandler.hasActiveSource(); this.runDetached((async () => { await this.videoHandler?.stopTranslate(); if (wasActive) await this.handleTranslationBtnClick(); })(), "Failed to restart translation after voice mode change"); }); } bindSettingsViewEvents() { const settingsView = this.votSettingsView; if (!settingsView) return; settingsView.addEventListener("update:account", async (account) => { if (!this.videoHandler) return; this.videoHandler.votClient.provider.apiToken = account?.token; }).addEventListener("change:autoTranslate", async (checked) => { const videoHandler = this.videoHandler; if (checked && videoHandler && !videoHandler.hasActiveSource()) await this.handleTranslationBtnClick(); }).addEventListener("change:autoSubtitles", async (checked) => { if (!checked || !this.videoHandler?.videoData?.videoId) return; await this.videoHandler.refreshAutoSubtitlesForCurrentLangPair(); }).addEventListener("select:responseLanguageSubtitles", async () => { if (!this.videoHandler?.data.autoSubtitles || !this.videoHandler.videoData) return; await this.videoHandler.refreshAutoSubtitlesForCurrentLangPair(); }).addEventListener("change:showVideoVolume", () => { this.withInitializedOverlayView((overlayView) => { if (!overlayView.videoVolumeSlider || !overlayView.votButton) return; overlayView.videoVolumeSlider.container.hidden = !this.data.showVideoSlider || overlayView.votButton.status !== "success"; }); }).addEventListener("change:audioBooster", async () => { this.withInitializedOverlayView((overlayView) => { if (!overlayView.translationVolumeSlider) return; const currentVolume = overlayView.translationVolumeSlider.value; const maxVolume = this.data.audioBooster && !this.data.syncVolume ? 900 : 100; overlayView.translationVolumeSlider.max = maxVolume; const nextVolume = clamp(currentVolume, 0, maxVolume); overlayView.translationVolumeSlider.value = nextVolume; this.videoHandler?.onTranslationVolumeSliderSynced(nextVolume); this.videoHandler?.syncTranslationPlaybackVolume(); }); }).addEventListener("change:syncVolume", (checked) => { if (!this.videoHandler) return; this.videoHandler.setupAudioSettings(); this.withInitializedOverlayView((overlayView) => { const videoSlider = overlayView.videoVolumeSlider; const translationSlider = overlayView.translationVolumeSlider; if (!videoSlider || !translationSlider) return; const maxVolume = this.data.audioBooster && !checked ? 900 : 100; translationSlider.max = maxVolume; const nextTranslation = clamp(translationSlider.value, 0, maxVolume); translationSlider.value = nextTranslation; this.videoHandler.onTranslationVolumeSliderSynced(nextTranslation); this.videoHandler.syncTranslationPlaybackVolume(); if (!checked) return; this.videoHandler.resetVolumeLinkState(Number(videoSlider.value), nextTranslation); }); }).addEventListener("change:subtitlesHighlightWords", (checked) => { this.updateSubtitlesWidgetSetting(checked, this.data.highlightWords, (widget, value) => { widget.setHighlightWords(value); }); }).addEventListener("change:subtitlesSmartLayout", (checked) => { this.updateSubtitlesWidgetSetting(checked, this.data.subtitlesSmartLayout, (widget, value) => { widget.setSmartLayout(value); }); }).addEventListener("input:subtitlesMaxLength", (value) => { this.updateSubtitlesWidgetSetting(value, this.data.subtitlesMaxLength, (widget, nextValue) => { widget.setMaxLength(nextValue); }); }).addEventListener("input:subtitlesFontSize", (value) => { this.updateSubtitlesWidgetSetting(value, this.data.subtitlesFontSize, (widget, nextValue) => { widget.setFontSize(nextValue); }); }).addEventListener("select:subtitlesFontFamily", (item) => { this.updateSubtitlesWidgetSetting(item, this.data.subtitlesFontFamily, (widget, nextValue) => { widget.setFontFamily(nextValue); }); }).addEventListener("input:subtitlesBackgroundOpacity", (value) => { this.updateSubtitlesWidgetSetting(value, this.data.subtitlesOpacity, (widget, nextValue) => { widget.setOpacity(nextValue); }); }).addEventListener("change:proxyWorkerHost", (_value) => { if (!this.videoHandler) return; this.runDetached(this.videoHandler.handleProxySettingsChanged("proxyWorkerHost"), "Failed to apply proxyWorkerHost change"); }).addEventListener("select:proxyTranslationStatus", () => { if (!this.videoHandler) return; this.runDetached(this.videoHandler.handleProxySettingsChanged("proxyTranslationStatus"), "Failed to apply proxyTranslationStatus change"); }).addEventListener("change:useNewAudioPlayer", () => { this.restartAudioPlayer(); }).addEventListener("change:onlyBypassMediaCSP", () => { this.restartAudioPlayer(); }).addEventListener("select:translationTextService", () => { this.withSubtitlesWidget((widget) => { widget.resetTranslationContext(true); }); }).addEventListener("change:showPiPButton", () => { this.withInitializedOverlayView((overlayView) => { if (!overlayView.votButton) return; overlayView.votButton.pipButton.hidden = overlayView.votButton.separator2.hidden = !overlayView.pipButtonVisible; }); }).addEventListener("select:buttonPosition", (item) => { this.withInitializedOverlayView((overlayView) => { const preferredPosition = normalizeButtonPosition(this.data.buttonPos ?? item); const { position, direction } = overlayView.calcButtonLayout(preferredPosition); overlayView.updateButtonLayout(position, direction); }); }).addEventListener("select:menuLanguage", async () => { await this.reloadMenu(); }).addEventListener("click:bugReport", () => { if (!this.videoHandler) return; const params = new URLSearchParams(this.videoHandler.collectReportInfo()).toString(); globalThis.open(`${repositoryUrl}/issues/new?${params}`, "_blank")?.focus(); }).addEventListener("click:resetSettings", async () => { const valuesForClear = await votStorage.list(); await Promise.all(valuesForClear.map((key) => votStorage.delete(key))); await votStorage.set("compatVersion", actualCompatVersion); globalThis.location.reload(); }); } async handleDownloadTranslationClick() { const overlayView = this.votOverlayView; const videoHandler = this.videoHandler; const download = videoHandler?.downloadTranslation; if (!overlayView?.isInitialized() || !download || !videoHandler.videoData) return; const downloadVideoData = await this.getDownloadVideoData(videoHandler, download.videoId); if (!downloadVideoData) return; const downloadButton = overlayView.downloadTranslationButton; const downloadUrl = download.url; const filename = this.data.downloadWithName ? clearFileName(downloadVideoData.downloadTitle) : `translation_${downloadVideoData.videoId}`; const saveOptions = { preferShare: this.isLikelyMobileDownloadContext() }; const setProgress = (progress) => { if (downloadButton) downloadButton.progress = progress; }; setProgress(0); try { await this.downloadTranslationAudio(downloadUrl, filename, setProgress, saveOptions); } catch (err) { console.error("[VOT] Download translation failed:", err); if (!this.triggerUrlDownload(downloadUrl, `${filename}.mp3`)) globalThis.open(downloadUrl, "_blank")?.focus(); } finally { setProgress(0); } } async getDownloadVideoData(videoHandler, downloadVideoId) { if (videoHandler.videoData?.videoId !== downloadVideoId) { this.clearDownloadTranslation(videoHandler); return null; } let videoData; try { videoData = await videoHandler.getVideoData(); } catch (err) { debug.log("[VOT] Failed to refresh video data before download", err); return null; } if (videoData.videoId !== downloadVideoId) { this.clearDownloadTranslation(videoHandler); return null; } videoHandler.videoData = videoData; return videoData; } clearDownloadTranslation(videoHandler) { videoHandler.downloadTranslation = null; if (this.votOverlayView?.downloadTranslationButton) this.votOverlayView.downloadTranslationButton.hidden = true; } async downloadTranslationAudio(downloadUrl, filename, onProgress, saveOptions) { const response = await GM_fetch(downloadUrl, { timeout: 0 }); if (!response.ok) throw new Error(`HTTP ${response.status}`); await downloadTranslation(response, filename, onProgress, saveOptions); } async handleDownloadSubtitlesClick() { const videoHandler = this.videoHandler; if (!videoHandler?.yandexSubtitles || !videoHandler.videoData) return; const subsFormat = this.data.subtitlesDownloadFormat ?? "json"; const subsContent = serializeProcessedSubtitles(videoHandler.yandexSubtitles, subsFormat, { assTitle: videoHandler.videoData.localizedTitle ?? videoHandler.videoData.title ?? videoHandler.videoData.downloadTitle }); await downloadBlob(new Blob([subsFormat === "json" ? JSON.stringify(subsContent) : subsContent], { type: "text/plain" }), `${this.data.downloadWithName ? clearFileName(videoHandler.videoData.downloadTitle) : `subtitles_${videoHandler.videoData.videoId}`}.${subsFormat}`, { preferShare: this.isLikelyMobileDownloadContext() }); } async reloadMenu() { if (!this.votOverlayView?.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized"); const prevButtonOpacity = this.votOverlayView.votButton.opacity; const prevButtonHidden = this.votOverlayView.votButton.container.hidden; const prevMenuHidden = this.votOverlayView.votMenu.hidden; const prevButtonPos = normalizeButtonPosition(this.data.buttonPos); const settingsWasOpen = this.votSettingsView?.dialog?.container?.hidden === false; await this.videoHandler?.stopTranslation(); this.release(); this.initUI(); this.initUIEvents(); if (!this.videoHandler) return this; try { const { position, direction } = this.votOverlayView.calcButtonLayout(prevButtonPos); this.votOverlayView.updateButtonLayout(position, direction); this.votOverlayView.votMenu.hidden = prevMenuHidden; this.votOverlayView.votButton.container.hidden = prevButtonHidden; this.votOverlayView.votButton.opacity = prevButtonOpacity; } catch (err) { debug.warn("[VOT] Failed to restore overlay state after menu reload", err); } try { this.videoHandler.rebindOverlayVisibilityTargets(); } catch (err) { debug.warn("[VOT] Failed to rebind overlay visibility targets", err); } if (settingsWasOpen) try { this.votSettingsView?.open(); } catch (err) { debug.warn("[VOT] Failed to reopen settings after menu reload", err); } await this.videoHandler.updateSubtitlesLangSelect(); const widget = this.videoHandler.subtitlesWidget; if (widget) widget.resetTranslationContext(true); return this; } async handleTranslationBtnClick() { if (!this.votOverlayView?.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized"); await handleTranslationButtonCommand({ videoHandler: this.videoHandler, currentStatus: this.votOverlayView.votButton.status, currentLoading: this.votOverlayView.votButton.loading, transformBtn: (status, text) => { this.transformBtn(status, text); } }); return this; } isLoadingText(text) { const delayed = localizationProvider.get("TranslationDelayed"); return typeof text === "string" && (text.includes(localizationProvider.get("translationTake")) || (delayed ? text.includes(delayed) : false)); } transformBtn(status, text) { if (!this.votOverlayView?.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized"); this.votOverlayView.votButton.status = status; this.votOverlayView.votButton.loading = status === "error" && this.isLoadingText(text); this.votOverlayView.votButton.setText(text); this.votOverlayView.votButtonTooltip.setContent(text); const { voicePopover, votButtonTooltip } = this.votOverlayView; const centered = this.votOverlayView.votButton.direction !== "column"; if (status === "error") { if (!centered) { voicePopover?.cancelShow(); voicePopover?.hideNow(); this.votOverlayView.votButton.setVoiceMenuOpen(false); } votButtonTooltip.dismissImmediate(); this.votOverlayView.syncTranslateButtonTooltip(); } else { votButtonTooltip.dismissImmediate(); this.votOverlayView.syncTranslateButtonTooltip(); this.votOverlayView.rescheduleVoicePopoverIfHovered(); } return this; } release() { if (!this.isInitialized()) return this; this.votOverlayView.release(); this.votSettingsView.release(); destroyShadowMount(this.globalPortalMount); this.globalPortalMount = void 0; this.votGlobalPortal = void 0; this.initialized = false; return this; } withInitializedOverlayView(callback) { if (!this.votOverlayView?.isInitialized()) return; callback(this.votOverlayView); } withSubtitlesWidget(callback) { const widget = this.videoHandler?.subtitlesWidget; if (!widget) return; callback(widget); } updateSubtitlesWidgetSetting(nextValue, storedValue, apply) { this.withSubtitlesWidget((widget) => { apply(widget, storedValue ?? nextValue); }); } runDetached(task, errorMessage) { task.catch((err) => { debug.warn(`[VOT] ${errorMessage}`, err); }); } triggerUrlDownload(url, filename) { try { const a = document.createElement("a"); a.href = url; a.download = filename; a.target = "_blank"; a.rel = "noopener noreferrer"; a.style.display = "none"; document.body.appendChild(a); a.click(); a.remove(); return true; } catch { return false; } } isLikelyMobileDownloadContext() { if (this.videoHandler?.site.additionalData === "mobile") return true; return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches; } async restartAudioPlayer() { const videoHandler = this.videoHandler; if (!videoHandler) return; try { await videoHandler.stopTranslate(); videoHandler.createPlayer(); } catch (err) { debug.warn("[VOT] Failed to restart audio player", err); } } }; //#endregion //#region src/ui/overlayVisibilityController.ts function isDomNode(value) { return typeof Node !== "undefined" && value instanceof Node; } function isPointerEventType(type) { return type.startsWith("pointer"); } function isHoverPointerEvent(event) { return event?.pointerType !== "touch"; } /** * Centralizes overlay visibility behavior: showing, hiding and deadline checks. */ var OverlayVisibilityController = class { deps; hideDeadlineMs = 0; hideArmed = false; pointerInsideOverlay = false; unsubscribeChecker; constructor(deps) { this.deps = deps; this.unsubscribeChecker = this.deps.checker.subscribe(() => { this.onCheckerTick(); }); } /** * Ensures overlay is visible immediately and returns current view. */ show() { const view = this.getView(); if (!view) return null; view.updateButtonOpacity(1); return view; } /** * Cancels scheduled auto-hide. */ cancel() { this.hideDeadlineMs = 0; this.hideArmed = false; } release() { this.cancel(); this.pointerInsideOverlay = false; this.unsubscribeChecker(); } /** * Schedules overlay auto-hide after configured delay. */ queueAutoHide() { const view = this.show(); if (!view) return; if (this.isOverlayHoverActive(view)) { this.cancel(); return; } const delay = this.deps.getAutoHideDelay(); this.hideDeadlineMs = this.nowMs() + Math.max(0, delay); this.hideArmed = true; this.deps.checker.markActivity("overlay-queue-hide"); this.deps.checker.requestImmediateTick(); } /** * Handles pointer/focus interactions originating from overlay elements. */ handleOverlayInteraction(event) { const type = event?.type; if (!type) return; if (type === "focusin") { this.handleFocusIn(); return; } if (isPointerEventType(type)) { this.pointerInsideOverlay = isHoverPointerEvent(event); this.cancel(); this.show(); event.stopPropagation?.(); } } /** * Handles interactions from the broader host container (video, document etc.). */ handleHostInteraction(event) { const type = event?.type; if (!type) return; if (type === "focusin") { this.handleFocusIn(); return; } if (isPointerEventType(type)) { const target = event.composedPath?.()[0] ?? event.target; if (this.deps.isInteractiveNode(target)) { this.pointerInsideOverlay = isHoverPointerEvent(event); event.stopPropagation?.(); this.cancel(); this.show(); return; } this.pointerInsideOverlay = false; this.deps.checker.markActivity("overlay-host-pointer"); } this.queueAutoHide(); } /** * Hides overlay immediately without delay. */ hide() { this.hideArmed = false; this.hideDeadlineMs = 0; this.pointerInsideOverlay = false; this.getView()?.updateButtonOpacity(0); } /** * Schedules hide if focus/pointer leaves overlay tree entirely. */ scheduleHide(event) { if (!this.getView()) return; const type = event?.type; if (type === "pointerleave") { if (this.isLeaveInsideOverlay(event)) return; this.pointerInsideOverlay = false; } if (type === "focusout" && this.isFocusMovingInsideOverlay(event)) return; this.queueAutoHide(); } onCheckerTick() { if (!this.hideArmed || this.hideDeadlineMs <= 0) return; if (this.nowMs() + 2 < this.hideDeadlineMs) return; const view = this.getView(); if (!view) { this.hideArmed = false; return; } if (this.isOverlayHoverActive(view)) { debug.log("[OverlayVisibility] skip hide (pointer inside overlay)"); this.cancel(); view.updateButtonOpacity(1); return; } this.hideArmed = false; let active = null; if (typeof document !== "undefined" && typeof document.hasFocus === "function" && document.hasFocus()) active = getDeepActiveElement(document); if (active && this.deps.isInteractiveNode(active)) { if (document.documentElement.classList.contains("vot-keyboard-nav")) { debug.log("[OverlayVisibility] skip hide (keyboard focus inside overlay)"); return; } if (active instanceof HTMLElement) active.blur(); } view.updateButtonOpacity(0); } handleFocusIn() { this.cancel(); this.show(); this.deps.checker.markActivity("overlay-focus-in"); } getView() { const view = this.deps.getOverlayView(); return view?.isInitialized() ? view : null; } isOverlayHoverActive(view = this.getView()) { return Boolean(view && (this.pointerInsideOverlay || view.shouldKeepVisibleForInteraction())); } getRelatedNode(event) { const relatedTarget = event?.relatedTarget ?? null; return isDomNode(relatedTarget) ? relatedTarget : null; } isLeaveInsideOverlay(event) { const relatedNode = this.getRelatedNode(event); if (!relatedNode) return false; if (this.deps.isInteractiveNode(relatedNode)) return true; const currentTarget = event?.currentTarget; const currentNode = isDomNode(currentTarget) ? currentTarget : null; return Boolean(currentNode && containsCrossShadow(currentNode, relatedNode)); } isFocusMovingInsideOverlay(event) { const relatedNode = this.getRelatedNode(event); return Boolean(relatedNode && this.deps.isInteractiveNode(relatedNode)); } nowMs() { if (this.deps.nowMs) return this.deps.nowMs(); return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); } }; //#endregion //#region src/core/platformEvents.ts var defaultPlatformConfig = { allowTouchMoveHandler: true, disableContainerDrag: false }; var platformOverrides = { xvideos: { allowTouchMoveHandler: false }, youtube: { disableContainerDrag: true } }; function getPlatformEventConfig(host) { if (!host) return defaultPlatformConfig; const overrides = platformOverrides[host] ?? {}; return { ...defaultPlatformConfig, ...overrides }; } //#endregion //#region src/videoHandler/modules/ducking.ts var VOLUME_MIN_01 = 0; var VOLUME_MAX_01 = 1; var SMART_DUCKING_DEFAULT_CONFIG = Object.freeze({ tickMs: 50, thresholdOnRms: .012, thresholdOffRms: .009, rmsAttackTauMs: 60, rmsReleaseTauMs: 240, holdMs: 520, attackTauMs: 110, releaseTauMs: 600, maxDownPerSec: 3.5, maxUpPerSec: .9, rmsMissingGraceMs: 200, maxDtMs: 250, externalBaselineDelta01: .02, unduckTolerance01: .01, volumeStep01: VIDEO_VOLUME_STEP_01, applyDeltaThreshold01: VIDEO_VOLUME_STEP_01 / 2 }); function initSmartDuckingRuntime(baseline) { return { isDucked: false, speechGateOpen: false, rmsEnvelope: 0, baseline: normalizeVolume01(baseline), lastApplied: void 0, lastTickAt: 0, lastSoundAt: 0, rmsMissingSinceAt: null }; } function updateSpeechGate(input, runtime, config, now, hasRms) { const gateOpen = runtime.speechGateOpen; if (!input.smartEnabled) { runtime.lastSoundAt = now; runtime.rmsMissingSinceAt = null; return true; } if (input.audioIsPlaying && !hasRms) { runtime.rmsMissingSinceAt ??= now; if (gateOpen) runtime.lastSoundAt = now; if (runtime.rmsMissingSinceAt !== null && now - runtime.rmsMissingSinceAt >= config.rmsMissingGraceMs) { runtime.lastSoundAt = now; return true; } return gateOpen; } runtime.rmsMissingSinceAt = null; if (!input.audioIsPlaying) return gateOpen && now - runtime.lastSoundAt <= config.holdMs; if (!gateOpen && runtime.rmsEnvelope >= config.thresholdOnRms) { runtime.lastSoundAt = now; return true; } if (gateOpen && runtime.rmsEnvelope >= config.thresholdOffRms) { runtime.lastSoundAt = now; return true; } return gateOpen && now - runtime.lastSoundAt <= config.holdMs; } function resolveBaseline(runtime, currentVideoVolume, volumeOnStart, config) { if (runtime.isDucked && isFiniteNumber(runtime.lastApplied) && Math.abs(currentVideoVolume - runtime.lastApplied) > config.externalBaselineDelta01) runtime.baseline = currentVideoVolume; if (!runtime.isDucked) runtime.baseline = currentVideoVolume; const baseline = runtime.baseline ?? volumeOnStart ?? currentVideoVolume; runtime.baseline = baseline; return baseline; } function resolveDesiredVolume(runtime, gateOpen, currentVideoVolume, baseline, duckingTarget01, config) { const duckedTarget = Math.min(baseline, duckingTarget01); if (gateOpen) { runtime.isDucked = true; return duckedTarget; } if (runtime.isDucked && Math.abs(baseline - currentVideoVolume) < config.unduckTolerance01) runtime.isDucked = false; return baseline; } function smoothVolumeChange(desired, currentVideoVolume, dtMs, dtSec, config) { const smoothingTauMs = desired < currentVideoVolume ? config.attackTauMs : config.releaseTauMs; const smoothingAlpha = smoothingTauMs > 0 ? -Math.expm1(-dtMs / smoothingTauMs) : 1; let nextVolume = currentVideoVolume + (desired - currentVideoVolume) * smoothingAlpha; const maxDelta = (desired < currentVideoVolume ? config.maxDownPerSec : config.maxUpPerSec) * dtSec; if (maxDelta > 0) nextVolume = clamp(nextVolume, currentVideoVolume - maxDelta, currentVideoVolume + maxDelta); return clamp(nextVolume, VOLUME_MIN_01, VOLUME_MAX_01); } function buildVolumeDecision(runtime, currentVideoVolume, quantized, applyDeltaThreshold01) { if (Math.abs(quantized - currentVideoVolume) < applyDeltaThreshold01) { runtime.lastApplied = quantized; return { kind: "noop", runtime }; } if (!isFiniteNumber(runtime.lastApplied) || Math.abs(quantized - runtime.lastApplied) >= applyDeltaThreshold01) { runtime.lastApplied = quantized; return { kind: "apply", runtime, volume01: quantized }; } return { kind: "noop", runtime }; } function computeSmartDuckingStep(input, runtime, config = SMART_DUCKING_DEFAULT_CONFIG) { const nextRuntime = normalizeRuntime(runtime); const volumeOnStart = normalizeVolume01(input.volumeOnStart); if (!input.translationActive || !input.enabledAutoVolume) return { kind: "stop", runtime: nextRuntime, restoreVolume: nextRuntime.baseline ?? volumeOnStart }; const now = Number.isFinite(input.nowMs) ? input.nowMs : Date.now(); const dtMs = clamp(now - (nextRuntime.lastTickAt || now), 0, config.maxDtMs); const dtSec = dtMs / 1e3; nextRuntime.lastTickAt = now; const hasRms = isFiniteNumber(input.rms); const rmsValue = hasRms ? clamp(input.rms, VOLUME_MIN_01, VOLUME_MAX_01) : 0; const prevEnv = nextRuntime.rmsEnvelope; const envTauMs = rmsValue > prevEnv ? config.rmsAttackTauMs : config.rmsReleaseTauMs; const envAlpha = envTauMs > 0 ? -Math.expm1(-dtMs / envTauMs) : 1; nextRuntime.rmsEnvelope = clamp(prevEnv + (rmsValue - prevEnv) * envAlpha, VOLUME_MIN_01, VOLUME_MAX_01); const gateOpen = updateSpeechGate(input, nextRuntime, config, now, hasRms); nextRuntime.speechGateOpen = gateOpen; const currentVideoVolume = normalizeVolume01(input.currentVideoVolume); if (!isFiniteNumber(currentVideoVolume)) return { kind: "noop", runtime: nextRuntime }; const baseline = resolveBaseline(nextRuntime, currentVideoVolume, volumeOnStart, config); if (!input.hostVideoActive) { nextRuntime.lastApplied = currentVideoVolume; return { kind: "noop", runtime: nextRuntime }; } const desired = resolveDesiredVolume(nextRuntime, gateOpen, currentVideoVolume, baseline, normalizeVolume01(input.duckingTarget01) ?? baseline, config); const quantized = snapVolume01Towards(smoothVolumeChange(desired, currentVideoVolume, dtMs, dtSec, config), currentVideoVolume, desired, config.volumeStep01); const applyDeltaThreshold01 = config.applyDeltaThreshold01; return buildVolumeDecision(nextRuntime, currentVideoVolume, quantized, applyDeltaThreshold01); } function normalizeRuntime(runtime) { return { isDucked: Boolean(runtime.isDucked), speechGateOpen: Boolean(runtime.speechGateOpen), rmsEnvelope: normalizeVolume01(runtime.rmsEnvelope) ?? 0, baseline: normalizeVolume01(runtime.baseline), lastApplied: normalizeVolume01(runtime.lastApplied), lastTickAt: isFiniteNumber(runtime.lastTickAt) ? runtime.lastTickAt : 0, lastSoundAt: isFiniteNumber(runtime.lastSoundAt) ? runtime.lastSoundAt : 0, rmsMissingSinceAt: isFiniteNumber(runtime.rmsMissingSinceAt) ? runtime.rmsMissingSinceAt : null }; } function normalizeVolume01(value) { if (!isFiniteNumber(value)) return void 0; return clamp(value, VOLUME_MIN_01, VOLUME_MAX_01); } function isFiniteNumber(value) { return typeof value === "number" && Number.isFinite(value); } //#endregion //#region src/videoHandler/modules/smartDuckingRuntime.ts var SMART_DUCKING_TICK_MS = SMART_DUCKING_DEFAULT_CONFIG.tickMs; var smartDuckingAnalyserState = /* @__PURE__ */ new WeakMap(); function isAudioNode(node) { if (!node || typeof node !== "object") return false; const candidate = node; return typeof candidate.connect === "function" && typeof candidate.disconnect === "function"; } function getPlayerMediaElement(player) { return player?.audio ?? player?.audioElement; } function getNowMs() { return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); } function getAutoVolumeMode(handler) { if (!handler.data?.enabledAutoVolume) return "off"; if (handler.data?.syncVolume) return "classic"; return handler.data?.enabledSmartDucking ?? true ? "smart" : "classic"; } function getSmartDuckingAudioContext(handler) { return handler.audioPlayer?.audioContext ?? handler.audioContext; } function disconnectSmartDuckingAnalyser(state) { if (state.connectedInputNode && state.analyser) try { state.connectedInputNode.disconnect(state.analyser); } catch {} state.connectedInputNode = void 0; if (state.createdMediaSource) try { state.createdMediaSource.disconnect(); } catch {} state.createdMediaSource = void 0; if (state.analyser) try { state.analyser.disconnect(); } catch {} state.analyser = void 0; state.analyserFloatData = void 0; state.analyserData = void 0; state.mediaElement = void 0; state.audioContext = void 0; state.mediaSourceCreationFailed = false; } function releaseSmartDuckingAnalyser(handler) { const state = smartDuckingAnalyserState.get(handler); if (!state) return; disconnectSmartDuckingAnalyser(state); smartDuckingAnalyserState.delete(handler); } function resolveSmartDuckingInputNode(player, media, audioContext, state) { if (isAudioNode(player?.audioSource)) return player.audioSource; if (isAudioNode(player?.mediaElementSource)) return player.mediaElementSource; if (state.mediaSourceCreationFailed && state.mediaElement === media && state.audioContext === audioContext) return; if (state.createdMediaSource && state.mediaElement === media && state.audioContext === audioContext) return state.createdMediaSource; try { const source = audioContext.createMediaElementSource(media); state.createdMediaSource = source; state.mediaSourceCreationFailed = false; return source; } catch (err) { state.mediaSourceCreationFailed = true; debug.log("[SmartDucking] failed to create media source", err); return; } } function ensureAnalyserNode(state, audioContext) { if (!state.analyser) { const analyser = audioContext.createAnalyser(); analyser.fftSize = 512; state.analyser = analyser; } return state.analyser; } function reconnectAnalyser(inputNode, analyser, state, audioContext) { if (state.connectedInputNode) try { state.connectedInputNode.disconnect(analyser); } catch {} try { analyser.disconnect(); } catch {} try { inputNode.connect(analyser); state.connectedInputNode = inputNode; if (state.createdMediaSource === inputNode) try { analyser.connect(audioContext.destination); } catch (err) { debug.log("[SmartDucking] failed to bridge analyser output", err); } return true; } catch (err) { debug.log("[SmartDucking] failed to connect analyser", err); return false; } } function ensureSmartDuckingAnalyser(handler, player, media) { const audioContext = getSmartDuckingAudioContext(handler); if (!audioContext) return void 0; let state = smartDuckingAnalyserState.get(handler); if (!state) { state = {}; smartDuckingAnalyserState.set(handler, state); } if (state.mediaElement && state.mediaElement !== media || state.audioContext && state.audioContext !== audioContext) disconnectSmartDuckingAnalyser(state); state.mediaElement = media; state.audioContext = audioContext; const analyser = ensureAnalyserNode(state, audioContext); const inputNode = resolveSmartDuckingInputNode(player, media, audioContext, state); if (!inputNode) return void 0; if (state.connectedInputNode !== inputNode) { if (!reconnectAnalyser(inputNode, analyser, state, audioContext)) return void 0; } return { analyser, state }; } function readSmartDuckingRuntime(handler) { return { isDucked: handler.smartVolumeIsDucked, speechGateOpen: handler.smartVolumeSpeechGateOpen, rmsEnvelope: handler.smartVolumeRmsEnvelope, baseline: handler.smartVolumeDuckingBaseline, lastApplied: handler.smartVolumeLastApplied, lastTickAt: handler.smartVolumeLastTickAt, lastSoundAt: handler.smartVolumeLastSoundAt, rmsMissingSinceAt: handler.smartVolumeRmsMissingSinceAt }; } function writeSmartDuckingRuntime(handler, runtime) { handler.smartVolumeIsDucked = runtime.isDucked; handler.smartVolumeSpeechGateOpen = runtime.speechGateOpen; handler.smartVolumeRmsEnvelope = runtime.rmsEnvelope; handler.smartVolumeDuckingBaseline = runtime.baseline; handler.smartVolumeLastApplied = runtime.lastApplied; handler.smartVolumeLastTickAt = runtime.lastTickAt; handler.smartVolumeLastSoundAt = runtime.lastSoundAt; handler.smartVolumeRmsMissingSinceAt = runtime.rmsMissingSinceAt; } function restoreAutoVolumeMute(handler) { if (typeof handler.autoVolumeMutedOnStart !== "boolean") return; try { handler.setVideoMuted(handler.autoVolumeMutedOnStart); } catch {} handler.autoVolumeMutedOnStart = void 0; } function stopSmartVolumeDucking(handler, options = {}) { const { restoreVolume } = options; if (handler.smartVolumeDuckingInterval !== void 0) { clearTimeout(handler.smartVolumeDuckingInterval); handler.smartVolumeDuckingInterval = void 0; } const baseline = typeof restoreVolume === "number" ? restoreVolume : handler.smartVolumeDuckingBaseline ?? handler.volumeOnStart; if (typeof baseline === "number" && (typeof restoreVolume === "number" || handler.smartVolumeIsDucked)) try { handler.setVideoVolume(baseline); } catch {} restoreAutoVolumeMute(handler); releaseSmartDuckingAnalyser(handler); writeSmartDuckingRuntime(handler, initSmartDuckingRuntime()); } function scheduleNextSmartDuckingTick(handler) { if (typeof globalThis === "undefined") return; if (handler.smartVolumeDuckingInterval === void 0) return; handler.smartVolumeDuckingInterval = globalThis.setTimeout(() => { if (handler.smartVolumeDuckingInterval === void 0) return; try { smartDuckingTick(handler); } catch (err) { debug.log("[SmartDucking] tick failed, stopping smart ducking", err); stopSmartVolumeDucking(handler); return; } if (handler.smartVolumeDuckingInterval === void 0) return; scheduleNextSmartDuckingTick(handler); }, SMART_DUCKING_TICK_MS); } function startSmartVolumeDucking(handler) { if (typeof globalThis === "undefined") return; if (handler.smartVolumeDuckingInterval !== void 0) return; if (getAutoVolumeMode(handler) !== "smart") return; const currentVideoVolume = handler.getVideoVolume(); const baseline = typeof handler.smartVolumeDuckingBaseline === "number" ? handler.smartVolumeDuckingBaseline : currentVideoVolume; const runtime = initSmartDuckingRuntime(baseline); if (Number.isFinite(currentVideoVolume) && Number.isFinite(baseline) && currentVideoVolume < baseline - SMART_DUCKING_DEFAULT_CONFIG.externalBaselineDelta01) { const now = getNowMs(); runtime.isDucked = true; runtime.speechGateOpen = true; runtime.lastApplied = currentVideoVolume; runtime.lastSoundAt = now; } writeSmartDuckingRuntime(handler, runtime); handler.smartVolumeDuckingInterval = globalThis.setTimeout(() => {}, 0); clearTimeout(handler.smartVolumeDuckingInterval); scheduleNextSmartDuckingTick(handler); } function getTranslatedAudioRms(handler, media) { const player = handler.audioPlayer?.player; const analyserBundle = ensureSmartDuckingAnalyser(handler, player, media); if (!analyserBundle) return void 0; const { analyser, state } = analyserBundle; try { if (typeof analyser.getFloatTimeDomainData === "function") { let floatData = state.analyserFloatData; if (floatData?.length !== analyser.fftSize) { floatData = new Float32Array(analyser.fftSize); state.analyserFloatData = floatData; } analyser.getFloatTimeDomainData(floatData); let sum = 0; for (const value of floatData) sum += value * value; return clamp(Math.sqrt(sum / floatData.length), 0, 1); } let data = state.analyserData; if (data?.length !== analyser.fftSize) { data = new Uint8Array(analyser.fftSize); state.analyserData = data; } analyser.getByteTimeDomainData(data); let sum = 0; for (const rawValue of data) { const normalizedValue = (rawValue - 128) / 128; sum += normalizedValue * normalizedValue; } return clamp(Math.sqrt(sum / data.length), 0, 1); } catch { return; } } function smartDuckingTick(handler) { if (browserInfo.browser?.name === "Safari") { const targetVolume = clamp(handler.data?.autoVolume ?? 15, 0, 100) / 100; handler.setVideoVolume(targetVolume, { preserveYoutubeVolumeStorage: true }); return; } if (getAutoVolumeMode(handler) !== "smart") { setupAudioSettings.call(handler); return; } const player = handler.audioPlayer?.player; const media = getPlayerMediaElement(player); const audioIsPlaying = !!media && !media.paused && !media.muted && (media.volume ?? 1) > .001; const now = getNowMs(); const currentVideoVolume = handler.getVideoVolume(); const hostVideo = handler.video; const hostVideoActive = !(hostVideo && (hostVideo.paused || hostVideo.ended)); const dynamicDuckingTarget = clamp(handler.data?.autoVolume ?? 15, 0, 100) / 100; const rms = audioIsPlaying && media ? getTranslatedAudioRms(handler, media) : 0; const decision = computeSmartDuckingStep({ nowMs: now, translationActive: handler.hasActiveSource(), enabledAutoVolume: true, smartEnabled: true, audioIsPlaying, rms, currentVideoVolume, hostVideoActive, duckingTarget01: dynamicDuckingTarget, volumeOnStart: handler.volumeOnStart }, readSmartDuckingRuntime(handler), SMART_DUCKING_DEFAULT_CONFIG); switch (decision.kind) { case "stop": stopSmartVolumeDucking(handler, { restoreVolume: decision.restoreVolume }); return; case "apply": handler.setVideoVolume(decision.volume01, { preserveYoutubeVolumeStorage: true }); writeSmartDuckingRuntime(handler, decision.runtime); return; case "noop": writeSmartDuckingRuntime(handler, decision.runtime); return; default: throw new TypeError("Unhandled smart ducking decision"); } } function setupAudioSettings() { if (typeof this.data?.defaultVolume === "number") safeSetPlayerVolume(this.audioPlayer.player, this.data.defaultVolume / 100); const autoVolumeMode = getAutoVolumeMode(this); if (autoVolumeMode === "off") { stopSmartVolumeDucking(this, { restoreVolume: this.smartVolumeDuckingBaseline ?? this.volumeOnStart }); return; } const targetVolume = clamp(this.data.autoVolume ?? 15, 0, 100) / 100; if (!this.hasActiveSource()) return; if (targetVolume === 0) { if (this.smartVolumeDuckingInterval !== void 0) { clearTimeout(this.smartVolumeDuckingInterval); this.smartVolumeDuckingInterval = void 0; } if (typeof this.smartVolumeDuckingBaseline !== "number") this.smartVolumeDuckingBaseline = this.getVideoVolume(); if (typeof this.autoVolumeMutedOnStart !== "boolean") this.autoVolumeMutedOnStart = Boolean(this.isMuted()); this.setVideoVolume(0, { preserveYoutubeVolumeStorage: true }); this.setVideoMuted(true); writeSmartDuckingRuntime(this, initSmartDuckingRuntime(this.smartVolumeDuckingBaseline)); this.smartVolumeIsDucked = true; return; } restoreAutoVolumeMute(this); if (autoVolumeMode === "smart") { startSmartVolumeDucking(this); return; } if (this.smartVolumeDuckingInterval !== void 0) { clearTimeout(this.smartVolumeDuckingInterval); this.smartVolumeDuckingInterval = void 0; } if (typeof this.smartVolumeDuckingBaseline !== "number") this.smartVolumeDuckingBaseline = this.getVideoVolume(); const baseline = this.smartVolumeDuckingBaseline ?? this.getVideoVolume(); const nextVolume = Math.min(baseline, targetVolume); this.setVideoVolume(nextVolume, { preserveYoutubeVolumeStorage: true }); writeSmartDuckingRuntime(this, initSmartDuckingRuntime(this.smartVolumeDuckingBaseline)); this.smartVolumeIsDucked = true; } function applyManualVideoVolumeOverride(volume01) { if (!this.data?.enabledAutoVolume || !this.hasActiveSource()) return; const nextVolume = snapVolume01(volume01); this.smartVolumeDuckingBaseline = nextVolume; this.smartVolumeLastApplied = nextVolume; } //#endregion //#region src/videoHandler/modules/proxyShared.ts var AUDIO_SOURCE_PREFIX = "https://vtrans.s3-private.mds.yandex.net/tts/prod/"; var AUDIO_PROXY_PATH_PREFIX = "/video-translation/audio-proxy/"; var SUBTITLE_SOURCE_PREFIX = "https://brosubs.s3-private.mds.yandex.net/vtrans/"; var SUBTITLE_PROXY_PATH_PREFIX = "/video-subtitles/subtitles-proxy/"; function resolveProxyWorkerHost(host) { return host ?? "vot-worker.eu.cc"; } function isProxyClientEnabled(config) { return Boolean(config.translateProxyEnabled); } function isProxyRoutingEnabled(config) { return config.translateProxyEnabled === 2; } function shouldForceProxyClientGmXhr(config) { return Boolean(config.gmXhrSupported && isProxyClientEnabled(config)); } /** * Generic proxy URL rewriter. Replaces a known source prefix with the * proxy-worker host + path when proxy routing is enabled. * * Centralizes the shared logic previously duplicated across * proxifyYandexAudioUrl and proxifyYandexSubtitlesUrl. */ function proxifyUrl(url, config, sourcePrefix, proxyPathPrefix) { if (!isProxyRoutingEnabled(config) || !url.startsWith(sourcePrefix)) return url; return url.replace(sourcePrefix, `https://${resolveProxyWorkerHost(config.proxyWorkerHost)}${proxyPathPrefix}`); } function proxifyYandexAudioUrl(audioUrl, config) { return proxifyUrl(audioUrl, config, AUDIO_SOURCE_PREFIX, AUDIO_PROXY_PATH_PREFIX); } function unproxifyYandexAudioUrl(audioUrl) { const str = String(audioUrl || ""); if (!str) return str; try { const url = new URL(str); if (!url.pathname.startsWith(AUDIO_PROXY_PATH_PREFIX)) return str; url.host = "vtrans.s3-private.mds.yandex.net"; url.pathname = `/tts/prod/${url.pathname.slice(31).replace(/^\/+/, "")}`; url.protocol = "https:"; return url.toString(); } catch { return str; } } function isYandexAudioUrlOrProxy(url, config) { return url.startsWith(AUDIO_SOURCE_PREFIX) || url.startsWith(`https://${resolveProxyWorkerHost(config.proxyWorkerHost)}${AUDIO_PROXY_PATH_PREFIX}`); } function proxifyYandexSubtitlesUrl(subtitlesUrl, config) { return proxifyUrl(subtitlesUrl, config, SUBTITLE_SOURCE_PREFIX, SUBTITLE_PROXY_PATH_PREFIX); } //#endregion //#region src/videoHandler/modules/subtitlesShared.ts var DISABLED_SUBTITLES_VALUE = "disabled"; var SUBTITLES_INDEX_OPTION_PATTERN = /^\d+$/u; var [AUTO_SUBTITLE_LANGUAGE_VALUE, ORIGINAL_SUBTITLE_LANGUAGE_VALUE] = subtitleResponseLanguageModes; function getIndexedSubtitleDescriptors(subtitles) { const descriptors = []; for (let index = 0; index < subtitles.length; index += 1) { const descriptor = parseSubtitleDescriptor(subtitles[index]); if (!descriptor) continue; descriptors.push({ descriptor, index }); } return descriptors; } function parseSubtitlesOptionIndex(value) { if (!SUBTITLES_INDEX_OPTION_PATTERN.test(value)) return null; const parsed = Number(value); if (!Number.isSafeInteger(parsed)) return null; return parsed; } function getSubtitleDescriptorAtIndex(subtitles, index) { if (!Number.isInteger(index) || index < 0 || index >= subtitles.length) return null; return parseSubtitleDescriptor(subtitles[index]); } function createDisabledSubtitlesOption() { return { label: localizationProvider.get("VOTSubtitlesDisabled"), value: DISABLED_SUBTITLES_VALUE, selected: true, disabled: false }; } function buildSubtitleLabel(subtitle) { return `${localizationProvider.getLangLabel(subtitle.language)}${subtitle.translatedFromLanguage ? ` ${localizationProvider.get("VOTTranslatedFrom")} ${localizationProvider.getLangLabel(subtitle.translatedFromLanguage)}` : ""}${subtitle.source === "yandex" ? "" : `, ${globalThis.location.hostname}`}${subtitle.isAutoGenerated ? ` (${localizationProvider.get("VOTAutogenerated")})` : ""}`; } function buildSubtitlesSelectOptions(subtitleDescriptors) { const options = [createDisabledSubtitlesOption()]; for (const { descriptor, index } of subtitleDescriptors) options.push({ label: buildSubtitleLabel(descriptor), value: String(index), selected: false, disabled: false }); return options; } function getSelectedSubtitlesValue(selectedValues) { const first = selectedValues[Symbol.iterator]().next(); return first.done ? void 0 : first.value; } function normalizeLang(lang) { return (lang ?? "").toLowerCase(); } function baseLang(lang) { return normalizeLang(lang).split(/[-_]/)[0]; } function langMatches(candidate, desired) { if (!candidate || !desired) return false; const cand = normalizeLang(candidate); const want = normalizeLang(desired); return cand === want || baseLang(cand) === baseLang(want); } function resolveSubtitlesLanguage(preference, detectedLanguage, responseLanguage) { if (preference === ORIGINAL_SUBTITLE_LANGUAGE_VALUE) { const originalLanguage = normalizeLang(detectedLanguage); return originalLanguage && originalLanguage !== AUTO_SUBTITLE_LANGUAGE_VALUE ? originalLanguage : void 0; } if (typeof preference === "string" && preference && preference !== AUTO_SUBTITLE_LANGUAGE_VALUE) return normalizeLang(preference); return normalizeLang(responseLanguage) || normalizeLang(detectedLanguage); } function pickBestSubtitlesIndex(subtitles, fromLang, toLang) { if (!subtitles.length) return null; const from = normalizeLang(fromLang); const to = normalizeLang(toLang); const fromIsAuto = from === "auto" || from === ""; const fromBase = baseLang(from); const toBase = baseLang(to); const isYandex = (descriptor) => descriptor.source === "yandex"; const isAutoGenerated = (descriptor) => Boolean(descriptor.isAutoGenerated); const matchesPair = (descriptor, wantFrom, wantTo) => { if (!langMatches(descriptor.language, wantTo)) return false; if (fromIsAuto) return true; return langMatches(descriptor.translatedFromLanguage, wantFrom); }; const isSameLangOriginal = (descriptor, lang) => { if (!langMatches(descriptor.language, lang)) return false; if (!descriptor.translatedFromLanguage) return true; return langMatches(descriptor.translatedFromLanguage, lang); }; const find = (predicate) => subtitles.find(({ descriptor }) => predicate(descriptor))?.index ?? null; const findOtherTarget = () => { const otherTargetManual = find((descriptor) => !isYandex(descriptor) && langMatches(descriptor.language, to) && !isAutoGenerated(descriptor)); if (otherTargetManual != null) return otherTargetManual; return find((descriptor) => !isYandex(descriptor) && langMatches(descriptor.language, to) && isAutoGenerated(descriptor)); }; if (!fromIsAuto && fromBase && toBase && fromBase === toBase) { const nativeManual = find((descriptor) => isSameLangOriginal(descriptor, to) && !isAutoGenerated(descriptor)); if (nativeManual != null) return nativeManual; const nativeAuto = find((descriptor) => isSameLangOriginal(descriptor, to) && isAutoGenerated(descriptor)); if (nativeAuto != null) return nativeAuto; const otherTarget = findOtherTarget(); if (otherTarget != null) return otherTarget; const yandexTargetSameLang = find((descriptor) => isYandex(descriptor) && langMatches(descriptor.language, to)); if (yandexTargetSameLang != null) return yandexTargetSameLang; } const yandexPair = find((descriptor) => isYandex(descriptor) && matchesPair(descriptor, from, to)); if (yandexPair != null) return yandexPair; const yandexTarget = find((descriptor) => isYandex(descriptor) && langMatches(descriptor.language, to)); if (yandexTarget != null) return yandexTarget; const otherPair = find((descriptor) => !isYandex(descriptor) && matchesPair(descriptor, from, to)); if (otherPair != null) return otherPair; const otherTarget = findOtherTarget(); if (otherTarget != null) return otherTarget; return null; } //#endregion //#region src/videoHandler/modules/translationShared.ts function normalizeTranslationHelp(translationHelp) { return translationHelp ?? null; } async function requestTranslationAudio(requester, options) { const response = await requester.translateVideoImpl(options.videoData, options.requestLang, options.responseLang, normalizeTranslationHelp(options.translationHelp), !options.useAudioDownload, options.signal); if (!response?.url) return null; return { url: response.url, usedLivelyVoice: Boolean(response.usedLivelyVoice) }; } function buildTranslationCacheValue(options) { return { videoId: options.videoId, from: options.requestLang, to: options.responseLang, url: options.downloadTranslationUrl ?? options.fallbackUrl, useLivelyVoice: options.usedLivelyVoice }; } /** * Executes an async action with staleness guards before and after. * Returns true if the action completed without becoming stale. * * Centralizes the "check stale -> act -> re-check stale" pattern that was * previously duplicated across updateTranslationIfFresh and * requestAndApplyTranslation. */ async function withStaleGuard(actionContext, isActionStale, action) { if (isActionStale(actionContext)) return false; await action(); return !isActionStale(actionContext); } async function updateTranslationIfFresh(options) { return withStaleGuard(options.actionContext, options.isActionStale, () => options.updateTranslation(options.url, options.actionContext, options.usedLivelyVoice)); } function setTranslationCacheValue(options) { options.setTranslation(options.cacheKey, buildTranslationCacheValue({ videoId: options.videoId, requestLang: options.requestLang, responseLang: options.responseLang, fallbackUrl: options.fallbackUrl, downloadTranslationUrl: options.downloadTranslationUrl, usedLivelyVoice: options.usedLivelyVoice })); } //#endregion //#region src/videoHandler/modules/translationPlayback.ts async function resumePlayerAudioContextIfNeeded(handler) { const ctx = handler.audioPlayer?.audioContext; if (ctx?.state !== "suspended") return "not-needed"; const RESUME_TIMEOUT_MS = 1500; const resumePromise = (async () => { try { await ctx.resume(); return "resumed"; } catch (err) { debug.log("[updateTranslation] Failed to resume AudioContext", err); return "failed"; } })(); let timeoutId; const timeoutPromise = new Promise((resolve) => { timeoutId = setTimeout(() => resolve("timeout"), RESUME_TIMEOUT_MS); }); const result = await Promise.race([resumePromise, timeoutPromise]); if (timeoutId !== void 0) clearTimeout(timeoutId); if (result === "resumed") debug.log("[updateTranslation] AudioContext resumed"); else if (result === "timeout") debug.log("[updateTranslation] AudioContext resume timeout"); return result; } async function rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl) { if (!appliedSourceUrl || !handler.audioPlayer) return; const player = handler.audioPlayer.player; const currentSource = String(player.currentSrc || player.src || ""); if (handler.proxifyAudio(handler.unproxifyAudio(currentSource)) !== handler.proxifyAudio(handler.unproxifyAudio(appliedSourceUrl))) return; try { await player.clear(); player.src = ""; debug.log("[updateTranslation] cleared stale partially-applied source"); } catch (err) { debug.log("[updateTranslation] failed to clear stale source", err); } } async function validateAudioUrl(audioUrl, actionContext) { if (this.isActionStale(actionContext)) return audioUrl; return audioUrl; } function scheduleTranslationRefresh() { if (!this.videoData || this.videoData.isStream) return; if (!this.hasActiveSource()) return; this.refreshTranslationAudio().catch((error) => { debug.log("[scheduleTranslationRefresh] refresh failed", error); }); } async function handlePlaybackResumedTranslationRefresh() { if (!this.videoData || this.videoData.isStream) return; if (!this.hasActiveSource()) return; const videoId = this.videoData.videoId; if (!videoId) return; const normalizedTranslationHelp = normalizeTranslationHelp(this.videoData.translationHelp); const cacheKey = this.getTranslationCacheKey(videoId, this.translateFromLang, this.translateToLang, normalizedTranslationHelp); if (!this.cacheManager.getTranslation(cacheKey)?.url) { debug.log("[scheduleTranslationRefresh] translation cache expired after resume, refreshing now"); await this.refreshTranslationAudio(); } } async function requestApplyAndCacheTranslation(self, options) { const translateRes = await requestTranslationAudio(self.translationHandler, { videoData: options.videoData, requestLang: options.requestLang, responseLang: options.responseLang, translationHelp: options.translationHelp, useAudioDownload: Boolean(self.data?.useAudioDownload), signal: self.actionsAbortController.signal }); if (!translateRes) return null; if (!await updateTranslationIfFresh({ url: translateRes.url, actionContext: options.actionContext, usedLivelyVoice: translateRes.usedLivelyVoice, isActionStale: (ctx) => self.isActionStale(ctx), updateTranslation: (url, ctx, usedLivelyVoice) => self.updateTranslation(url, ctx, usedLivelyVoice) })) return null; if (options.onBeforeCache) await options.onBeforeCache(translateRes); setTranslationCacheValue({ cacheKey: options.cacheKey, setTranslation: (key, value) => self.cacheManager.setTranslation(key, value), videoId: options.cacheVideoId, requestLang: options.cacheRequestLang, responseLang: options.cacheResponseLang, fallbackUrl: translateRes.url, downloadTranslationUrl: self.downloadTranslation?.url, usedLivelyVoice: translateRes.usedLivelyVoice }); return translateRes; } async function refreshTranslationAudio() { if (!this.videoData || this.videoData.isStream) return; if (!this.hasActiveSource()) return; if (this.isRefreshingTranslation) return; const videoId = this.videoData.videoId; if (!videoId) return; if (this.actionsAbortController?.signal?.aborted) this.resetActionsAbortController("refreshTranslationAudio"); this.isRefreshingTranslation = true; const actionContext = { gen: this.actionsGeneration, videoId }; const normalizedTranslationHelp = normalizeTranslationHelp(this.videoData.translationHelp); try { if (!await requestApplyAndCacheTranslation(this, { videoData: this.videoData, requestLang: this.translateFromLang, responseLang: this.translateToLang, translationHelp: normalizedTranslationHelp, actionContext, cacheKey: this.getTranslationCacheKey(videoId, this.translateFromLang, this.translateToLang, normalizedTranslationHelp), cacheVideoId: videoId, cacheRequestLang: this.translateFromLang, cacheResponseLang: this.translateToLang })) return; } finally { this.isRefreshingTranslation = false; } } function proxifyAudio(audioUrl) { const proxiedAudioUrl = proxifyYandexAudioUrl(audioUrl, { translateProxyEnabled: this.data?.translateProxyEnabled, proxyWorkerHost: this.data?.proxyWorkerHost }); if (proxiedAudioUrl !== audioUrl) debug.log(`[VOT] Audio proxied via ${proxiedAudioUrl}`); return proxiedAudioUrl; } function unproxifyAudio(audioUrl) { return unproxifyYandexAudioUrl(audioUrl); } async function handleProxySettingsChanged(reason = "proxySettingsChanged") { debug.log(`[VOT] ${reason}: clearing translation/subtitles cache`); try { this.cacheManager.clear(); this.activeTranslation = null; } catch {} try { await this.stopTranslation(); } catch {} await this.initVOTClient(); } function isMultiMethodS3(url) { return isYandexAudioUrlOrProxy(url, { proxyWorkerHost: this.data?.proxyWorkerHost }); } function normalizeManagedAudioUrl(handler, url) { return handler.proxifyAudio(handler.unproxifyAudio(url)); } async function applyTranslationSource(handler, sourceUrl, actionContext) { const didSetSource = handler.audioPlayer.player.src !== sourceUrl; let appliedSourceUrl = null; if (didSetSource) { handler.audioPlayer.player.src = sourceUrl; appliedSourceUrl = sourceUrl; } try { if (didSetSource) await handler.audioPlayer.init(); if (handler.isActionStale(actionContext)) { await rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl); return { status: "stale", didSetSource, appliedSourceUrl }; } const resumeResult = await resumePlayerAudioContextIfNeeded(handler); if (resumeResult === "timeout") debug.log("[updateTranslation] continuing after AudioContext resume timeout"); else if (resumeResult === "failed") debug.log("[updateTranslation] AudioContext resume failed, continue without deferred resume"); if (handler.isActionStale(actionContext)) { await rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl); return { status: "stale", didSetSource, appliedSourceUrl }; } if (!handler.video.paused && handler.audioPlayer.player.src) handler.audioPlayer.player.lipSync("play"); return { status: "success", didSetSource, appliedSourceUrl }; } catch (error) { return { status: "error", didSetSource, appliedSourceUrl, error }; } } function getTranslationActiveVoiceLabel(usedLivelyVoice) { return localizationProvider.get(usedLivelyVoice ? "VOTLiveVoicesTitle" : "VOTStandardVoicesTitle"); } async function updateTranslation(audioUrl, actionContext, usedLivelyVoice = this.data?.useLivelyVoice !== false) { await this.waitForPendingStopTranslate(); if (this.isActionStale(actionContext)) return; if (!this.audioPlayer) this.createPlayer(); if (this.audioPlayer.audioContext?.state === "closed") { debug.log("[updateTranslation] AudioContext is closed, recreating player"); this.createPlayer(); } const normalizedTargetUrl = normalizeManagedAudioUrl(this, audioUrl); if (this.isActionStale(actionContext)) return; const resolvedSource = await applyTranslationWithDirectFallback(this, normalizedTargetUrl, actionContext); const resolvedAudioUrl = resolvedSource.nextAudioUrl; const applyResult = resolvedSource.applyResult; const appliedSourceUrl = applyResult.appliedSourceUrl; if (applyResult.status === "stale") return; if (applyResult.status === "error") { debug.log("this.audioPlayer.init() error", applyResult.error); await rollbackStaleAppliedSourceIfStillCurrent(this, appliedSourceUrl); const msg = toErrorMessage(applyResult.error); this.transformBtn("error", msg); return; } this.clearVolumeLinkState(); this.setupAudioSettings(); this.transformBtn("success", getTranslationActiveVoiceLabel(usedLivelyVoice)); this.afterUpdateTranslation(resolvedAudioUrl); } function syncTranslationPlaybackVolume() { const player = this.audioPlayer?.player; const nextVolume = this.uiManager.votOverlayView?.translationVolumeSlider?.value; applyTranslationPlaybackVolume(player, nextVolume, this.data?.defaultVolume); } async function applyTranslationWithDirectFallback(handler, audioUrl, actionContext) { const nextAudioUrl = audioUrl; const applyResult = await applyTranslationSource(handler, nextAudioUrl, actionContext); if (!shouldRetryTranslationSource(handler, applyResult, actionContext, nextAudioUrl)) return { nextAudioUrl, applyResult }; const retried = await retryTranslationWithDirectSource(handler, nextAudioUrl, applyResult.appliedSourceUrl, actionContext); if (retried) return retried; return { nextAudioUrl, applyResult }; } function shouldRetryTranslationSource(handler, applyResult, actionContext, audioUrl) { return applyResult.status === "error" && applyResult.didSetSource && !handler.isActionStale(actionContext) && handler.unproxifyAudio(audioUrl) !== audioUrl; } async function retryTranslationWithDirectSource(handler, audioUrl, appliedSourceUrl, actionContext) { const directUrl = handler.unproxifyAudio(audioUrl); debug.log("[updateTranslation] proxied audio init failed, retrying direct URL"); try { if (handler.isActionStale(actionContext)) { await rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl); return { nextAudioUrl: directUrl, applyResult: { status: "stale", didSetSource: true, appliedSourceUrl } }; } return { nextAudioUrl: directUrl, applyResult: await applyTranslationSource(handler, directUrl, actionContext) }; } catch (fallbackErr) { return { nextAudioUrl: audioUrl, applyResult: { status: "error", didSetSource: true, appliedSourceUrl, error: fallbackErr } }; } } async function translateFunc(VIDEO_ID, _isStream, requestLang, responseLang, translationHelp) { await this.waitForPendingStopTranslate(); debug.log("Run videoValidator"); await this.videoValidator(); if (this.actionsAbortController?.signal?.aborted) this.resetActionsAbortController("translateFunc"); const overlayView = this.uiManager.votOverlayView; if (!overlayView?.votButton) { debug.log("[translateFunc] Overlay view missing, skipping translation"); return; } overlayView.votButton.loading = true; this.hadAsyncWait = false; this.volumeOnStart = this.getVideoVolume(); if (!VIDEO_ID) { debug.log("Skip translation - no VIDEO_ID resolved yet"); await this.updateTranslationErrorMsg(new VOTLocalizedError("VOTNoVideoIDFound"), this.actionsAbortController.signal); return; } const videoData = this.videoData; if (!videoData) { await this.updateTranslationErrorMsg(new VOTLocalizedError("VOTNoVideoIDFound"), this.actionsAbortController.signal); return; } const normalizedTranslationHelp = normalizeTranslationHelp(translationHelp); const cacheKey = this.getTranslationCacheKey(VIDEO_ID, requestLang, responseLang, normalizedTranslationHelp); const activeKey = `video_${cacheKey}`; if (this.activeTranslation?.key === activeKey) { debug.log("[translateFunc] Reusing in-flight translation"); await this.activeTranslation.promise; return; } const actionContext = { gen: this.actionsGeneration, videoId: VIDEO_ID }; const translationPromise = (async () => { if (this.isActionStale(actionContext)) { debug.log("[translateFunc] Stale translation task - skipping"); return; } const reqLang = requestLang; const resLang = responseLang; const applyTranslationUrl = async (url, usedLivelyVoice) => await this.updateTranslation(url, actionContext, usedLivelyVoice); const cachedEntry = this.cacheManager.getTranslation(cacheKey); if (cachedEntry?.url) { if (!await withStaleGuard(actionContext, (ctx) => this.isActionStale(ctx), () => applyTranslationUrl(cachedEntry.url, cachedEntry.useLivelyVoice))) return; debug.log("[translateFunc] Cached translation was received"); return; } const translateRes = await requestApplyAndCacheTranslation(this, { videoData, requestLang: reqLang, responseLang: resLang, translationHelp: normalizedTranslationHelp, actionContext, cacheKey, cacheVideoId: VIDEO_ID, cacheRequestLang: requestLang, cacheResponseLang: responseLang, onBeforeCache: async () => { const preferredSubtitleLanguage = this.getPreferredSubtitlesLanguage(videoData.detectedLanguage, videoData.responseLanguage); if (!preferredSubtitleLanguage) return; const subsCacheKey = this.videoData ? this.getSubtitlesCacheKey(VIDEO_ID, this.videoData.detectedLanguage, preferredSubtitleLanguage) : null; const cachedSubs = subsCacheKey ? this.cacheManager.getSubtitles(subsCacheKey) : null; if (!(Array.isArray(cachedSubs) && pickBestSubtitlesIndex(getIndexedSubtitleDescriptors(cachedSubs), videoData.detectedLanguage, preferredSubtitleLanguage) != null)) { if (subsCacheKey) this.cacheManager.deleteSubtitles(subsCacheKey); this.subtitles = []; this.subtitlesCacheKey = null; } } }); debug.log("[translateRes]", translateRes); if (!translateRes) debug.log("Skip translation"); })(); this.activeTranslation = { key: activeKey, promise: translationPromise }; try { return await translationPromise; } catch (err) { this.hadAsyncWait = notifyTranslationFailureIfNeeded({ aborted: this.actionsAbortController.signal.aborted, translateApiErrorsEnabled: Boolean(this.data?.translateAPIErrors), hadAsyncWait: this.hadAsyncWait, videoId: VIDEO_ID, error: err, notify: (params) => this.notifier.translationFailed(params) }); throw err; } finally { if (this.activeTranslation?.promise === translationPromise) this.activeTranslation = null; const overlayBtn = this.uiManager.votOverlayView?.votButton; if (!this.activeTranslation && overlayBtn?.loading && !this.hasActiveSource()) { debug.log("[translateFunc] clearing stale loading state"); this.transformBtn("none", localizationProvider.get("translateVideo")); } } } function isYouTubeHosts() { return isTranslationDownloadHost(this.site.host); } //#endregion //#region src/videoHandler/modules/events.ts function mergeListenerSignals(primary, secondary) { if (!secondary || secondary === primary) return primary; const signals = [primary, secondary]; if (typeof AbortSignal.any === "function") return AbortSignal.any(signals); const controller = new AbortController(); for (const signal of signals) { const abort = () => controller.abort(signal.reason); if (signal.aborted) { abort(); break; } signal.addEventListener("abort", abort, { once: true, signal: controller.signal }); } return controller.signal; } function createScopedListeners(signal) { const add = (element, event, handler, options) => { element.addEventListener(event, handler, { ...options, signal: mergeListenerSignals(signal, options?.signal) }); }; const addMany = (element, events, handler, options) => { for (const event of events) add(element, event, handler, options); }; return { add, addMany }; } function bindOverlayHoverFocusEvents(addMany, target, overlayVisibility) { const handleInteraction = (event) => overlayVisibility.handleOverlayInteraction(event); const scheduleHide = (event) => overlayVisibility.scheduleHide(event); if (isIframe() && globalThis.window !== void 0) { addMany(target, ["focusin"], handleInteraction); addMany(target, ["focusout"], scheduleHide); return; } addMany(target, ["focusin", "pointerenter"], handleInteraction); addMany(target, ["pointermove"], handleInteraction, { passive: true }); addMany(target, ["focusout", "pointerleave"], scheduleHide); } function toPercentInt(value, fallback = 0) { const numeric = typeof value === "number" ? value : Number(value); return Number.isFinite(numeric) ? clampPercentInt(numeric) : fallback; } function syncAudioTranslationVolumeFromVideo(self, videoPercent, options = {}) { if (options.skipYouTubeLikeHosts && isYouTubeLikeHost(self.site.host)) return; if (self.smartVolumeDuckingInterval !== void 0) return; if (!self.data?.syncVolume || !self.audioPlayer?.player?.src) return; if (self.isLikelyInternalVideoVolumeChange(videoPercent)) return; self.syncVolumeWrapper("video", videoPercent); } function applyOverlayLayout(self, overlayView, heightPx) { const menu = overlayView.votMenu?.container; if (menu) { let height; if (heightPx) height = heightPx; else if (self.fullscreenHelper) { const target = self.fullscreenHelper.getResizeObserverTarget(); height = target.getBoundingClientRect().height || target.clientHeight || window.innerHeight * .75; } else height = self.video.getBoundingClientRect().height; if (!height || height < 200) height = window.innerHeight * .75; menu.style.setProperty("--vot-container-height", `${height}px`); } const { position, direction } = overlayView.calcButtonLayout(self.data?.buttonPos ?? "default"); overlayView.updateButtonLayout(position, direction); } function normalizeHotkeyPart(value) { return value.replace("Key", "").replace("Digit", ""); } function buildPressedHotkeyPartsSet(userPressedKeys) { const pressedParts = /* @__PURE__ */ new Set(); for (const key of userPressedKeys) pressedParts.add(normalizeHotkeyPart(key)); return pressedParts; } function getParsedHotkey(hotkey, cache) { if (!hotkey) return null; const cached = cache.get(hotkey); if (cached) return cached; const parts = hotkey.split("+").filter(Boolean).map(normalizeHotkeyPart); const parsed = { parts, partsSet: new Set(parts) }; cache.set(hotkey, parsed); return parsed; } function isHotkeyMatch(pressedParts, hotkey) { if (!hotkey) return false; if (pressedParts.size !== hotkey.parts.length) return false; for (const key of hotkey.partsSet) if (!pressedParts.has(key)) return false; return true; } function bindOverlayLayoutEvents(ctx) { const { self, overlayView, addMany } = ctx; const syncMountAndLayout = () => { self.refreshOverlayMount(); applyOverlayLayout(self, overlayView); }; self.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) applyOverlayLayout(self, overlayView, entry.contentRect.height); }); self.resizeObserver.observe(self.video); syncMountAndLayout(); addMany(document, ["fullscreenchange", "webkitfullscreenchange"], () => syncMountAndLayout()); addMany(self.video, ["webkitbeginfullscreen", "webkitendfullscreen"], () => syncMountAndLayout()); } function bindYouTubeVolumeSync(ctx) { const { self } = ctx; if (!isDesktopYouTubeLikeSite(self.site)) return; self.syncVolumeObserver = new MutationObserver((mutations) => { if (!self.audioPlayer?.player?.src) return; let hasVolumeMutation = false; for (const mutation of mutations) { if (mutation.type !== "attributes" || mutation.attributeName !== "aria-valuenow") continue; hasVolumeMutation = true; } if (!hasVolumeMutation) return; self.syncVideoVolumeSlider(); const activeOverlayView = self.uiManager.votOverlayView; if (!activeOverlayView?.isInitialized()) return; const videoPercent = toPercentInt(activeOverlayView.videoVolumeSlider.value); syncAudioTranslationVolumeFromVideo(self, videoPercent); }); const ytpVolumePanel = document.querySelector(".ytp-volume-panel"); if (!ytpVolumePanel) return; self.syncVolumeObserver.observe(ytpVolumePanel, { attributes: true, subtree: true, attributeFilter: ["aria-valuenow"] }); } function bindAudioTrackLanguageSync(ctx) { const { self } = ctx; if (self.site.host !== "youtube" || self.site.additionalData === "mobile") return; let lastSyncedAudioTrackLanguage; const syncAudioTrackLanguage = async () => { try { if (!self.videoData) return; const player = YoutubeHelper.getPlayer(); const availableTracks = player?.getAvailableAudioTracks?.() ?? null; if (!Array.isArray(availableTracks) || availableTracks.length <= 1) return; const currentTrackId = (player?.getAudioTrack?.()?.getLanguageInfo?.())?.id; const currentLanguageCode = currentTrackId && currentTrackId !== "und" ? currentTrackId.toLowerCase().split(/[-_.]/)[0] : void 0; if (!currentLanguageCode) return; if (!availableLangs.includes(currentLanguageCode)) return; const currentLanguage = currentLanguageCode; if (currentLanguage === lastSyncedAudioTrackLanguage) return; lastSyncedAudioTrackLanguage = currentLanguage; if (currentLanguage === self.videoData.detectedLanguage) return; self.videoManager.rememberDetectedLanguage(self.videoData.videoId, currentLanguage); self.setSelectMenuValues(currentLanguage, self.videoData.responseLanguage); if (self.data?.autoTranslate && currentLanguage !== self.videoData.responseLanguage) { debug.log(`[VOT] Audio track language changed to ${currentLanguage}, triggering auto-translation`); try { await self.uiManager.handleTranslationBtnClick(); } catch (error) { debug.log("[VOT] Failed to trigger auto-translation on audio track change:", error); } } } catch (error) { debug.log("[VOT] Failed to sync audio track language", error); } }; const player = YoutubeHelper.getPlayer(); const listeners = ["onApiChange", "onStateChange"]; if (player?.addEventListener) for (const eventName of listeners) try { player.addEventListener(eventName, syncAudioTrackLanguage); } catch (error) { debug.log(`[VOT] Failed to bind ${eventName}`, error); } syncAudioTrackLanguage(); self.abortController.signal.addEventListener("abort", () => { if (!player?.removeEventListener) return; for (const eventName of listeners) try { player.removeEventListener(eventName, syncAudioTrackLanguage); } catch (error) { debug.log(`[VOT] Failed to unbind ${eventName}`, error); } }, { once: true }); } function bindGlobalDismissAndHotkeys(ctx) { const { self, overlayView, add, addMany, platformConfig } = ctx; add(document, "click", (event) => { const target = event.target; const button = overlayView.votButton?.container; const menu = overlayView.votMenu?.container; const settings = self.uiManager.votSettingsView?.dialog?.container; const path = event.composedPath(); const isInPath = (element) => Boolean(element && path.includes(element)); const isButton = isInPath(button); const isMenu = isInPath(menu); const isVideo = isInPath(self.container); const isSettings = isInPath(settings); const isTempDialog = target instanceof Element && target.closest(".vot-dialog-temp") instanceof Element; debug.log(`[document click] ${isButton} ${isMenu} ${isVideo} ${isSettings} ${isTempDialog}`); if (isButton || isMenu || isSettings || isTempDialog) return; if (!isVideo) overlayView.updateButtonOpacity(0); if (menu && !menu.hidden) { menu.hidden = true; self.overlayVisibility?.queueAutoHide(); } }); const userPressedKeys = /* @__PURE__ */ new Set(); const hotkeyCache = /* @__PURE__ */ new Map(); const clearUserPressedKeys = () => userPressedKeys.clear(); const runHotkeyAction = (action, actionName) => { action().catch((error) => { debug.log(`[VOT] ${actionName} hotkey action failed`, error); }); }; add(document, "keydown", (event) => { const keyboardEvent = event; if (keyboardEvent.repeat) return; userPressedKeys.add(keyboardEvent.code); const activeElement = getDeepActiveElement(document); const activeTag = activeElement?.tagName?.toLowerCase?.() ?? ""; if (["input", "textarea"].includes(activeTag) || Boolean(activeElement?.isContentEditable)) return; const pressedParts = buildPressedHotkeyPartsSet(userPressedKeys); if (isHotkeyMatch(pressedParts, getParsedHotkey(self.data?.translationHotkey, hotkeyCache))) { clearUserPressedKeys(); runHotkeyAction(() => self.uiManager.handleTranslationBtnClick(), "Translation"); return; } if (isHotkeyMatch(pressedParts, getParsedHotkey(self.data?.subtitlesHotkey, hotkeyCache))) { clearUserPressedKeys(); runHotkeyAction(() => self.toggleSubtitlesForCurrentLangPair(), "Subtitles"); } }); add(document, "keyup", (event) => userPressedKeys.delete(event.code)); add(document, "blur", clearUserPressedKeys); add(document, "visibilitychange", () => { if (document.hidden) clearUserPressedKeys(); }); add(globalThis, "blur", clearUserPressedKeys); const eventContainer = self.getEventContainer(); if (eventContainer) { const useWindowEvents = isIframe() && globalThis.window !== void 0; const interactionTarget = useWindowEvents ? globalThis.window : eventContainer; if (useWindowEvents) { addMany(interactionTarget, ["pointermove", "pointerdown"], (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true }); add(interactionTarget, "blur", () => self.overlayVisibility.scheduleHide()); } else { addMany(interactionTarget, ["pointerenter", "pointerdown"], (event) => self.overlayVisibility.handleHostInteraction(event)); add(interactionTarget, "pointermove", (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true }); } } self.rebindOverlayVisibilityTargets(); if (platformConfig.allowTouchMoveHandler) add(document, "touchmove", (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true }); if (platformConfig.disableContainerDrag) self.container.draggable = false; } function bindPlaybackRefreshOnResume(ctx) { const { self, add } = ctx; let wasPausedSinceLastPlay = false; const resetPauseState = () => { wasPausedSinceLastPlay = false; }; add(self.video, "pause", () => { wasPausedSinceLastPlay = true; }); add(self.video, "playing", () => { if (!wasPausedSinceLastPlay) return; wasPausedSinceLastPlay = false; handlePlaybackResumedTranslationRefresh.call(self).catch((error) => { debug.log("[VOT] Failed to refresh translation after playback resumed", error); }); }); add(self.video, "loadstart", resetPauseState); add(self.video, "emptied", resetPauseState); } function bindVideoLifecycleEvents(ctx) { const { self, overlayView, add } = ctx; const safeSetCanPlay = async () => { try { await self.setCanPlay(); } catch (err) { debug.log("[VOT] setCanPlay() failed", err); } }; let setCanPlayQueued = false; const queueSetCanPlay = () => { if (setCanPlayQueued) return; setCanPlayQueued = true; queueMicrotask(async () => { setCanPlayQueued = false; await safeSetCanPlay(); }); }; let emptiedHandled = false; add(self.video, "canplay", () => { emptiedHandled = false; if (self.site.host === "rutube" && self.video.src) return; queueSetCanPlay(); }); const handleVideoEmptied = async () => { if (emptiedHandled) return; emptiedHandled = true; let videoId; try { videoId = await getVideoID(self.site, { fetchFn: GM_fetch, video: self.video }); } catch (error) { debug.log("[VOT] Failed to resolve video id on emptied", error); } if (self.videoData && videoId && videoId === self.videoData.videoId) { emptiedHandled = false; return; } debug.log("lipsync mode is emptied"); resetAndHideLifecycle(self, overlayView, { clearVideoData: true, hideMenu: true }); }; add(self.video, "emptied", () => { handleVideoEmptied().catch((error) => { debug.log("[VOT] Failed to handle emptied lifecycle event", error); }); }); if (!isMuteSyncDisabledHost(self.site.host)) add(self.video, "volumechange", () => { self.syncVideoVolumeSlider(); const activeOverlayView = self.uiManager.votOverlayView; if (!activeOverlayView?.isInitialized()) return; const videoPercent = toPercentInt(activeOverlayView.videoVolumeSlider.value); syncAudioTranslationVolumeFromVideo(self, videoPercent, { skipYouTubeLikeHosts: true }); }); if (self.site.host === "youtube" && !self.site.additionalData) add(document, "yt-page-data-updated", () => { debug.log("yt-page-data-updated"); if (!globalThis.location.pathname.startsWith("/shorts/")) return; queueSetCanPlay(); }); } function initExtraEvents() { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.subtitlesSelect) return; const { add, addMany } = createScopedListeners(this.abortController.signal); const ctx = { self: this, overlayView, platformConfig: getPlatformEventConfig(this.site.host), add, addMany }; bindPlaybackRefreshOnResume(ctx); bindOverlayLayoutEvents(ctx); bindYouTubeVolumeSync(ctx); bindAudioTrackLanguageSync(ctx); bindGlobalDismissAndHotkeys(ctx); bindVideoLifecycleEvents(ctx); } function rebindOverlayVisibilityTargets() { this.overlayVisibilityTargetsAbortController?.abort(); this.overlayVisibilityTargetsAbortController = new AbortController(); const { signal } = this.overlayVisibilityTargetsAbortController; const overlayView = this.uiManager?.votOverlayView; const overlayButton = overlayView?.votButton?.container; const overlayMenu = overlayView?.votMenu?.container; if (!overlayButton || !overlayMenu || !this.overlayVisibility) return; const overlayVisibility = this.overlayVisibility; const { addMany } = createScopedListeners(signal); bindOverlayHoverFocusEvents(addMany, overlayButton, overlayVisibility); bindOverlayHoverFocusEvents(addMany, overlayMenu, overlayVisibility); const voicePopoverContainer = overlayView?.voicePopover?.container; if (voicePopoverContainer) bindOverlayHoverFocusEvents(addMany, voicePopoverContainer, overlayVisibility); } function isOverlayInteractiveNode(node) { if (!(node instanceof Node)) return false; const overlayView = this.uiManager?.votOverlayView; const buttonContainer = overlayView?.votButton?.container; const menuContainer = overlayView?.votMenu?.container; const voicePopoverContainer = overlayView?.voicePopover?.container; return buttonContainer instanceof Node && containsCrossShadow(buttonContainer, node) || menuContainer instanceof Node && containsCrossShadow(menuContainer, node) || voicePopoverContainer instanceof Node && containsCrossShadow(voicePopoverContainer, node); } function getAutoHideDelay() { const delay = this.data?.autoHideButtonDelay; return typeof delay === "number" && Number.isFinite(delay) ? delay : defaultAutoHideDelay; } function releaseExtraEvents() { this.resizeObserver?.disconnect(); this.overlayVisibilityTargetsAbortController?.abort(); this.overlayVisibilityTargetsAbortController = void 0; if (isDesktopYouTubeLikeSite(this.site)) this.syncVolumeObserver?.disconnect(); } //#endregion //#region src/videoHandler/modules/init.ts var countryCodeRequestInFlight = null; async function ensureCountryCode() { if (getCountryCode()) return; countryCodeRequestInFlight ??= (async () => { try { setCountryCode((await (await GM_fetch("https://cloudflare-dns.com/cdn-cgi/trace", { timeout: 7e3 })).text()).split("\n").find((line) => line.startsWith("loc="))?.slice(4, 6).toUpperCase()); } catch (err) { console.error("[VOT] Error getting country:", err); } })().finally(() => { countryCodeRequestInFlight = null; }); await countryCodeRequestInFlight; } async function init() { if (this.initialized) return; const audioContextSupported = this.isAudioContextSupported; this.data = await votStorage.getValues({ autoTranslate: false, autoSubtitles: false, dontTranslateLanguages: [calculatedResLang], enabledDontTranslateLanguages: true, enabledAutoVolume: true, enabledSmartDucking: true, autoVolume: 15, buttonPos: "default", showVideoSlider: true, syncVolume: false, downloadWithName: isSupportGMXhr, sendNotifyOnComplete: false, subtitlesMaxLength: 300, subtitlesSmartLayout: true, highlightWords: false, subtitlesFontSize: 20, subtitlesFontFamily: "default-sans", subtitlesOpacity: 20, subtitlesDownloadFormat: "srt", responseLanguage: calculatedResLang, responseLanguageSubtitles: "auto", defaultVolume: 100, onlyBypassMediaCSP: audioContextSupported, newAudioPlayer: audioContextSupported, showPiPButton: false, translateAPIErrors: true, translationService: defaultTranslationService, detectService: defaultDetectService, translationHotkey: null, subtitlesHotkey: null, m3u8ProxyHost, proxyWorkerHost, translateProxyEnabled: 0, translateProxyEnabledDefault: true, audioBooster: false, useLivelyVoice: false, autoHideButtonDelay: defaultAutoHideDelay, useAudioDownload: isSupportGMXhr, compatVersion: "", account: {}, localeHash: "", localeUpdatedAt: 0 }); if (this.data.compatVersion !== "2025-05-09") { this.data = await updateConfig(this.data); await votStorage.set("compatVersion", actualCompatVersion); } try { if (calculatedResLang === "en" && this.data?.enabledDontTranslateLanguages && Array.isArray(this.data?.dontTranslateLanguages) && this.data.dontTranslateLanguages.length === 1 && this.data.dontTranslateLanguages[0] === "en" && typeof this.data.responseLanguage === "string" && this.data.responseLanguage !== "en") { const responseLang = this.data.responseLanguage; this.data.dontTranslateLanguages = [responseLang]; await votStorage.set("dontTranslateLanguages", this.data.dontTranslateLanguages); } } catch {} this.uiManager.data = this.data; console.log("[VOT] data from db:", this.data); if (!this.data.translateProxyEnabled && isProxyOnlyExtension) this.data.translateProxyEnabled = 1; await ensureCountryCode(); const countryCode = getCountryCode(); if (countryCode !== null && proxyOnlyCountries.includes(countryCode) && this.data.translateProxyEnabledDefault) this.data.translateProxyEnabled = 2; debug.log("translateProxyEnabled", this.data.translateProxyEnabled, this.data.translateProxyEnabledDefault); debug.log("Extension compatibility passed..."); await this.initVOTClient(); this.uiManager.initUI(); this.uiManager.initUIEvents(); if (this.uiManager.votOverlayView?.votButton?.container) this.uiManager.votOverlayView.votButton.container.hidden = true; this.createPlayer(); this.translateToLang = this.data.responseLanguage ?? "ru"; this.initExtraEvents(); this.initialized = true; } //#endregion //#region src/subtitles/segmenter.ts var HAS_SEGMENTER = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"; var DEFAULT_CACHE_LOCALE = "und"; var segmenterCache = /* @__PURE__ */ new Map(); var resolvedLocaleCache = /* @__PURE__ */ new Map(); var canonicalizeLocale = (locale) => { if (!locale) return void 0; try { return Intl.getCanonicalLocales(locale)[0]; } catch { return; } }; var resolveSegmenterLocale = (locale) => { const cacheKey = locale ?? DEFAULT_CACHE_LOCALE; if (resolvedLocaleCache.has(cacheKey)) return resolvedLocaleCache.get(cacheKey); const canonicalLocale = canonicalizeLocale(locale); if (!canonicalLocale) { resolvedLocaleCache.set(cacheKey, void 0); return; } const resolvedLocale = Intl.Segmenter.supportedLocalesOf([canonicalLocale])[0]; resolvedLocaleCache.set(cacheKey, resolvedLocale); return resolvedLocale; }; var getSegmenter = (locale, granularity) => { const resolvedLocale = resolveSegmenterLocale(locale); const cacheKey = `${granularity}:${resolvedLocale ?? DEFAULT_CACHE_LOCALE}`; const cached = segmenterCache.get(cacheKey); if (cached) return cached; const segmenter = new Intl.Segmenter(resolvedLocale, { granularity }); segmenterCache.set(cacheKey, segmenter); return segmenter; }; var fallbackSegmentText = (text) => { const result = []; const regex = /([\p{L}\p{N}]+)|([^\p{L}\p{N}]+)/gu; let match = regex.exec(text); while (match !== null) { result.push({ text: match[0], index: match.index, isWordLike: match[1] !== void 0 }); match = regex.exec(text); } return result; }; var fallbackSegmentSentences = (text) => { const result = []; const regex = /[^.!?\n]+(?:[.!?\n]+\s*)*|.+/gu; let match = regex.exec(text); while (match !== null) { if (!match[0]) break; result.push({ text: match[0], index: match.index }); match = regex.exec(text); } return result; }; var segmentText = (text, locale) => { if (!text) return []; if (HAS_SEGMENTER) return Array.from(getSegmenter(locale, "word").segment(text), (part) => ({ text: part.segment, index: part.index, isWordLike: Boolean(part.isWordLike) })); return fallbackSegmentText(text); }; var segmentSentences = (text, locale) => { if (!text) return []; if (HAS_SEGMENTER) return Array.from(getSegmenter(locale, "sentence").segment(text), (part) => ({ text: part.segment, index: part.index })); return fallbackSegmentSentences(text); }; //#endregion //#region src/subtitles/textSpacing.ts var WHITESPACE_RE$1 = /\s/u; var NO_SPACE_BEFORE_RE = /^[,.:;!?%)\]}>»]/u; var NO_SPACE_AFTER_RE = /[([{<«'"-]$/u; var CJK_CHAR_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u; var shouldInsertSpaceBetweenTextFragments = (leftText, rightText) => { const leftLastChar = leftText.at(-1) ?? ""; const rightFirstChar = rightText[0] ?? ""; if (!leftLastChar || !rightFirstChar) return false; if (WHITESPACE_RE$1.test(leftLastChar) || WHITESPACE_RE$1.test(rightFirstChar)) return false; if (NO_SPACE_AFTER_RE.test(leftLastChar) || NO_SPACE_BEFORE_RE.test(rightFirstChar)) return false; if (CJK_CHAR_RE.test(leftLastChar) && CJK_CHAR_RE.test(rightFirstChar)) return false; return true; }; //#endregion //#region src/subtitles/autoMerge.ts var WHITESPACE_RE = /\s/u; var hasVisibleText = (value) => Boolean(value.trim()); var buildNormalizedLineSpans = (lines) => { let streamText = ""; let previousText = ""; const spans = []; for (const line of lines) { const normalizedText = line.text.trim(); if (!normalizedText) continue; if (streamText && shouldInsertSpaceBetweenTextFragments(previousText, normalizedText)) streamText += " "; const start = streamText.length; streamText += normalizedText; const end = streamText.length; spans.push({ line: { ...line, text: normalizedText }, text: normalizedText, start, end }); previousText = normalizedText; } return { streamText, spans }; }; var trimSentenceRange = (streamText, start, end) => { let trimmedStart = start; let trimmedEnd = end; while (trimmedStart < trimmedEnd && WHITESPACE_RE.test(streamText[trimmedStart] ?? "")) trimmedStart += 1; while (trimmedEnd > trimmedStart && WHITESPACE_RE.test(streamText[trimmedEnd - 1] ?? "")) trimmedEnd -= 1; if (trimmedStart >= trimmedEnd) return null; return { start: trimmedStart, end: trimmedEnd }; }; var sliceLineToken = (span, rangeStart, rangeEnd) => { const overlapStart = Math.max(rangeStart, span.start); const overlapEnd = Math.min(rangeEnd, span.end); if (overlapStart >= overlapEnd) return null; const localStart = overlapStart - span.start; const localEnd = overlapEnd - span.start; const text = span.text.slice(localStart, localEnd); if (!text) return null; const textLength = Math.max(span.text.length, 1); const startMs = span.line.startMs + Math.round(span.line.durationMs * localStart / textLength); const endMs = span.line.startMs + Math.round(span.line.durationMs * localEnd / textLength); return { text, startMs, durationMs: Math.max(0, endMs - startMs), isWordLike: hasVisibleText(text) }; }; var buildSentenceLine = (streamText, spans, segment) => { const rawStart = segment.index; const trimmedRange = trimSentenceRange(streamText, rawStart, rawStart + segment.text.length); if (!trimmedRange) return null; const text = streamText.slice(trimmedRange.start, trimmedRange.end); if (!hasVisibleText(text)) return null; const tokens = []; let speakerId = "0"; for (const span of spans) { const token = sliceLineToken(span, trimmedRange.start, trimmedRange.end); if (!token) continue; if (tokens.length === 0) speakerId = span.line.speakerId; tokens.push(token); } if (!tokens.length) return null; const startMs = Math.min(...tokens.map((token) => token.startMs)); const endMs = Math.max(...tokens.map((token) => token.startMs + Math.max(0, token.durationMs))); return { text, startMs, durationMs: Math.max(0, endMs - startMs), speakerId, tokens }; }; var mergeAutoGeneratedSubtitleLines = (lines, locale) => { const { streamText, spans } = buildNormalizedLineSpans(lines); if (!streamText || !spans.length) return []; const sentenceLines = segmentSentences(streamText, locale).map((segment) => buildSentenceLine(streamText, spans, segment)).filter((line) => line !== null); return sentenceLines.length ? sentenceLines : spans.map(({ line }) => line); }; //#endregion //#region src/subtitles/processor.ts var toFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0; var toNonNegativeNumber = (value) => Math.max(0, toFiniteNumber(value)); var pickDescriptorFromVideoData = (videoData, requestLang, spokenLang) => { const list = videoData.subtitles; if (!Array.isArray(list) || list.length === 0) return null; const desiredLang = requestLang ?? spokenLang; if (desiredLang) { const translated = list.find((subtitle) => subtitle.language === desiredLang && typeof subtitle.translatedFromLanguage === "string"); if (translated) return translated; const original = list.find((subtitle) => subtitle.language === desiredLang); if (original) return original; } return list[0] ?? null; }; var isVideoDataForSubtitles = (value) => "host" in value && "videoId" in value && "detectedLanguage" in value && "duration" in value && typeof value.host === "string" && typeof value.videoId === "string" && typeof value.detectedLanguage === "string" && typeof value.duration === "number"; var appendYoutubePoTokenParams = (inputUrl) => { const poToken = YoutubeHelper.getPoToken(); if (!poToken) return inputUrl; let normalizedPoToken = poToken; for (let i = 0; i < 10; i += 1) { let nextValue; try { nextValue = decodeURIComponent(normalizedPoToken); } catch { break; } if (nextValue === normalizedPoToken) break; normalizedPoToken = nextValue; } const deviceParamsRaw = YoutubeHelper.getDeviceParams(); const normalizedDeviceParams = typeof deviceParamsRaw === "string" ? deviceParamsRaw.replace(/^[?&]+/u, "") : ""; try { const parsed = new URL(inputUrl); parsed.searchParams.set("potc", "1"); parsed.searchParams.set("pot", normalizedPoToken); if (normalizedDeviceParams) { const deviceParams = new URLSearchParams(normalizedDeviceParams); for (const [key, value] of deviceParams.entries()) parsed.searchParams.set(key, value); } return parsed.toString(); } catch { const separator = inputUrl.includes("?") ? "&" : "?"; const serializedDeviceParams = normalizedDeviceParams ? `&${normalizedDeviceParams}` : ""; return `${inputUrl}${separator}potc=1&pot=${encodeURIComponent(normalizedPoToken)}${serializedDeviceParams}`; } }; var compareNumbers = (left, right) => left - right; var compareStrings = (left, right) => { if (left < right) return -1; if (left > right) return 1; return 0; }; var compareRankArrays = (left, right) => { const length = Math.min(left.length, right.length); for (let i = 0; i < length; i += 1) { const diff = left[i] - right[i]; if (diff !== 0) return diff; } if (left.length !== right.length) return left.length - right.length; return 0; }; var getYandexTranslationKindRank = (isYandex, requestLanguageRank, isTranslated) => { if (!isYandex) return 0; if (requestLanguageRank === 0) return isTranslated ? 1 : 0; return isTranslated ? 0 : 1; }; var getTranslatedFromRequestRank = (isYandex, isTranslated, translatedFromLanguage, requestLang) => { if (!isYandex || !isTranslated) return 0; return translatedFromLanguage === requestLang ? 0 : 1; }; var buildSubtitleRank = (descriptor, requestLang) => { const isYandex = descriptor.source === "yandex"; const sourceRank = isYandex ? 0 : 1; const uiLanguageRank = descriptor.language === lang ? 0 : 1; const isTranslated = Boolean(descriptor.translatedFromLanguage); const requestLanguageRank = requestLang && descriptor.language === requestLang ? 0 : 1; return [ sourceRank, uiLanguageRank, getYandexTranslationKindRank(isYandex, requestLanguageRank, isTranslated), getTranslatedFromRequestRank(isYandex, isTranslated, descriptor.translatedFromLanguage, requestLang), isYandex && !isTranslated ? requestLanguageRank : 0, isYandex ? 0 : Number(Boolean(descriptor.isAutoGenerated)) ]; }; var rankSubtitle = (descriptor, index, requestLang) => ({ descriptor, index, rank: buildSubtitleRank(descriptor, requestLang), language: descriptor.language, translatedFromLanguage: descriptor.translatedFromLanguage ?? "", source: descriptor.source, url: descriptor.url, isAutoGenerated: Number(Boolean(descriptor.isAutoGenerated)) }); var sortSubtitles = (subtitles, requestLang) => { const ranked = subtitles.map((descriptor, index) => rankSubtitle(descriptor, index, requestLang)); ranked.sort((left, right) => { const rankDiff = compareRankArrays(left.rank, right.rank); if (rankDiff !== 0) return rankDiff; const descriptorOrder = compareStrings(left.language, right.language) || compareStrings(left.translatedFromLanguage, right.translatedFromLanguage) || compareStrings(left.source, right.source) || compareStrings(left.url, right.url) || compareNumbers(left.isAutoGenerated, right.isAutoGenerated); if (descriptorOrder !== 0) return descriptorOrder; return compareNumbers(left.index, right.index); }); return ranked.map((entry) => entry.descriptor); }; var resolveTokenWordLike = (value, text) => { if (typeof value === "boolean") return value; return Boolean(text.trim()); }; var sanitizeStyledSpan = (span) => { if (!span || typeof span !== "object") return null; const raw = span; const start = toNonNegativeNumber(raw.start); const end = toNonNegativeNumber(raw.end); if (end <= start) return null; return { start, end, style: sanitizeSubtitleInlineStyle(raw.style) }; }; var sanitizeLineMetadata = (value) => { if (!value || typeof value !== "object") return; const raw = value; const metadata = {}; if (typeof raw.rawText === "string") metadata.rawText = raw.rawText; const styledSpans = Array.isArray(raw.styledSpans) ? raw.styledSpans.map(sanitizeStyledSpan).filter((span) => span !== null) : []; if (styledSpans.length) metadata.styledSpans = styledSpans; if (raw.vtt && typeof raw.vtt === "object") metadata.vtt = raw.vtt; if (raw.ass && typeof raw.ass === "object") metadata.ass = raw.ass; return Object.keys(metadata).length ? metadata : void 0; }; var sanitizeToken = (token) => { if (!token || typeof token !== "object") return { text: "", startMs: 0, durationMs: 0, isWordLike: false }; const raw = token; const text = typeof raw.text === "string" ? raw.text : ""; return { text, startMs: toNonNegativeNumber(raw.startMs), durationMs: toNonNegativeNumber(raw.durationMs), isWordLike: resolveTokenWordLike(raw.isWordLike, text), style: sanitizeSubtitleInlineStyle(raw.style) }; }; var sanitizeLine = (line) => { if (!line || typeof line !== "object") return { text: "", startMs: 0, durationMs: 0, speakerId: "0", tokens: [] }; const raw = line; const tokens = Array.isArray(raw.tokens) ? raw.tokens.map(sanitizeToken) : []; return { text: typeof raw.text === "string" ? raw.text : "", startMs: toNonNegativeNumber(raw.startMs), durationMs: toNonNegativeNumber(raw.durationMs), speakerId: typeof raw.speakerId === "string" ? raw.speakerId : "0", tokens, metadata: sanitizeLineMetadata(raw.metadata) }; }; var ensureProcessedSubtitles = (input) => { if (!input || typeof input !== "object") return { format: "json", subtitles: [] }; const payload = input; const subtitles = Array.isArray(payload.subtitles) ? payload.subtitles.map(sanitizeLine) : []; return { format: payload.format ?? "json", subtitles }; }; var getYoutubeEventDurationMs = (event, nextEvent) => { if (!nextEvent) return Math.max(0, event.dDurationMs); if (event.tStartMs + event.dDurationMs <= nextEvent.tStartMs) return Math.max(0, event.dDurationMs); return Math.max(0, nextEvent.tStartMs - event.tStartMs); }; var buildYoutubeSourceTokens = (event, segs, durationMs) => { const sourceTokens = []; let text = ""; let remainingDuration = durationMs; let previousRawText = ""; for (let j = 0; j < segs.length; j += 1) { const segment = segs[j]; const rawText = typeof segment.utf8 === "string" ? segment.utf8 : ""; if (!rawText) continue; const offset = Math.max(0, segment.tOffsetMs ?? 0); let segmentDuration = durationMs; const nextSegment = segs[j + 1]; if (nextSegment?.tOffsetMs !== void 0) { const nextOffset = Math.max(offset, nextSegment.tOffsetMs); segmentDuration = Math.max(0, nextOffset - offset); remainingDuration = Math.max(remainingDuration - segmentDuration, 0); } let tokenDuration = Math.max(0, remainingDuration); if (nextSegment) tokenDuration = Math.max(0, segmentDuration); if (text && shouldInsertSpaceBetweenTextFragments(previousRawText, rawText)) { sourceTokens.push({ text: " ", startMs: event.tStartMs + offset, durationMs: 0, isWordLike: false }); text += " "; } sourceTokens.push({ text: rawText, startMs: event.tStartMs + offset, durationMs: tokenDuration, isWordLike: Boolean(rawText.trim()) }); text += rawText; previousRawText = rawText; } return { text, sourceTokens }; }; var hasPositiveDuration = (token) => token.durationMs > 0; var normalizeLineText = (line) => { if (line.text) return normalizeSubtitleTextForDisplay(line.text); if (!line.tokens.length) return ""; return normalizeSubtitleTextForDisplay(line.tokens.map((token) => token.text).join("")); }; var allocateTimingsByLength = (texts, startMs, durationMs) => { if (!texts.length) return []; const safeDuration = Math.max(0, durationMs); const weights = texts.map((text) => Math.max(text.length, 1)); const totalWeight = weights.reduce((sum, weight) => sum + weight, 0); const prefixWeights = new Array(weights.length + 1).fill(0); for (let i = 0; i < weights.length; i += 1) prefixWeights[i + 1] = prefixWeights[i] + weights[i]; const timings = []; for (let i = 0; i < texts.length; i += 1) { const from = Math.round(safeDuration * prefixWeights[i] / totalWeight); const to = i === texts.length - 1 ? safeDuration : Math.round(safeDuration * prefixWeights[i + 1] / totalWeight); timings.push({ startMs: startMs + from, durationMs: Math.max(0, to - from) }); } return timings; }; var splitSegmentText = (text) => { const slices = []; let sliceStart = 0; for (let index = 0; index < text.length; index += 1) { if (text[index] !== "\n") continue; if (sliceStart < index) slices.push({ text: text.slice(sliceStart, index), startOffset: sliceStart, endOffset: index }); slices.push({ text: "\n", startOffset: index, endOffset: index + 1 }); sliceStart = index + 1; } if (sliceStart < text.length) slices.push({ text: text.slice(sliceStart), startOffset: sliceStart, endOffset: text.length }); return slices.length ? slices : [{ text, startOffset: 0, endOffset: text.length }]; }; var getNormalizedTokenText = (tokens) => normalizeSubtitleTextForDisplay(tokens.map((token) => token.text).join("")); var normalizeExistingTokensForDisplay = (tokens) => { const normalizedTokens = []; for (const token of tokens) { if (!token.text.includes("\n")) { normalizedTokens.push(token); continue; } const slices = splitSegmentText(token.text); const sliceTimings = allocateTimingsByLength(slices.map((slice) => slice.text === "\n" ? "" : slice.text), token.startMs, token.durationMs); for (let index = 0; index < slices.length; index += 1) { const slice = slices[index]; const isLineBreak = slice.text === "\n"; normalizedTokens.push({ ...token, text: slice.text, startMs: sliceTimings[index]?.startMs ?? token.startMs, durationMs: isLineBreak ? 0 : sliceTimings[index]?.durationMs ?? 0, isWordLike: isLineBreak ? false : token.isWordLike }); } } return normalizedTokens; }; var canReuseExistingTokens = (line, descriptor, lineText) => { if (!lineText || !line.tokens.length || descriptor.source === "youtube") return false; if (line.metadata?.styledSpans?.length && !line.tokens.some((token) => token.style)) return false; return getNormalizedTokenText(line.tokens) === lineText; }; var collectSourceTimedWords = (sourceTokens, locale) => { const timedWords = []; for (const token of sourceTokens) { const normalizedTokenText = normalizeSubtitleTextForDisplay(token.text); if (!normalizedTokenText || !hasPositiveDuration(token)) continue; const segmentedWords = segmentText(normalizedTokenText, locale).filter((segment) => segment.isWordLike && segment.text.trim()); if (!segmentedWords.length) continue; const segmentTimings = allocateTimingsByLength(segmentedWords.map((segment) => segment.text), token.startMs, token.durationMs); timedWords.push(...segmentTimings); } return timedWords; }; var buildDirectSegmentToken = (segment, segmentStartMs, segmentDurationMs, styledSpans) => ({ text: segment.text, startMs: segmentStartMs, durationMs: segmentDurationMs, isWordLike: segment.isWordLike, style: getStyleForRange(styledSpans, segment.index, segment.index + segment.text.length) }); var buildSegmentSliceTokens = (segment, segmentStartMs, segmentDurationMs, styledSpans) => { const slices = splitSegmentText(segment.text); if (slices.length === 1 && slices[0].text === segment.text) return [buildDirectSegmentToken(segment, segmentStartMs, segmentDurationMs, styledSpans)]; const sliceTimings = allocateTimingsByLength(slices.map((slice) => slice.text === "\n" ? "" : slice.text), segmentStartMs, segmentDurationMs); return slices.map((slice, sliceIndex) => { const isLineBreak = slice.text === "\n"; return { text: slice.text, startMs: sliceTimings[sliceIndex]?.startMs ?? segmentStartMs, durationMs: isLineBreak ? 0 : sliceTimings[sliceIndex]?.durationMs ?? 0, isWordLike: !isLineBreak && segment.isWordLike, style: isLineBreak ? void 0 : getStyleForRange(styledSpans, segment.index + slice.startOffset, segment.index + slice.endOffset) }; }); }; var collectWordIndices = (tokens) => tokens.reduce((indices, token, index) => { if (token.isWordLike && token.text.trim()) indices.push(index); return indices; }, []); var applySourceWordTimings = (tokens, sourceTimedWords) => { if (!sourceTimedWords.length) return tokens; const wordIndices = collectWordIndices(tokens); if (!wordIndices.length) return tokens; const totalTargetWords = wordIndices.length; for (let i = 0; i < totalTargetWords; i += 1) { const targetIndex = wordIndices[i]; const sourceIndex = Math.floor(i * sourceTimedWords.length / totalTargetWords); const sourceTiming = sourceTimedWords[Math.min(sourceIndex, sourceTimedWords.length - 1)]; tokens[targetIndex] = { ...tokens[targetIndex], startMs: sourceTiming.startMs, durationMs: sourceTiming.durationMs }; } return tokens; }; var buildLineTokens = (line, descriptor, lineText) => { if (!lineText) return []; if (canReuseExistingTokens(line, descriptor, lineText)) return normalizeExistingTokensForDisplay(line.tokens); const locale = descriptor.language; const styledSpans = line.metadata?.styledSpans ?? buildStyledDisplayModel(line.metadata?.rawText ?? line.text ?? lineText).styledSpans; const segments = segmentText(lineText, locale); if (!segments.length) return []; const baseTimings = allocateTimingsByLength(segments.map((segment) => segment.text), line.startMs, line.durationMs); const nextTokens = []; for (let index = 0; index < segments.length; index += 1) { const segment = segments[index]; const segmentStartMs = baseTimings[index]?.startMs ?? line.startMs; const segmentDurationMs = baseTimings[index]?.durationMs ?? 0; nextTokens.push(...buildSegmentSliceTokens(segment, segmentStartMs, segmentDurationMs, styledSpans)); } return applySourceWordTimings(nextTokens, collectSourceTimedWords(line.tokens, locale)); }; var fetchRawSubtitles = async (url, format) => { const response = await GM_fetch(url, { timeout: 7e3 }); if (format === "vtt" || format === "srt" || format === "ass") return parseSubtitleText(await response.text(), format); return response.json(); }; var normalizeFetchedSubtitles = (rawSubtitles, descriptor) => { if (descriptor.source === "youtube") return SubtitlesProcessor.formatYoutubeSubtitles(rawSubtitles, Boolean(descriptor.isAutoGenerated)); if (descriptor.format === "srt" || descriptor.format === "vtt" || descriptor.format === "ass") return sortProcessedSubtitles(ensureProcessedSubtitles(rawSubtitles)); return sortProcessedSubtitles(ensureProcessedSubtitles(rawSubtitles)); }; var processFetchedSubtitles = (subtitles, descriptor) => { const maybeMerged = SubtitlesProcessor.autoMerge(subtitles, descriptor); return { ...maybeMerged, subtitles: SubtitlesProcessor.processTokens(maybeMerged, descriptor) }; }; var buildYandexSubtitles = (response) => { const subtitles = []; const seenOriginal = /* @__PURE__ */ new Set(); for (const subtitle of response.subtitles ?? []) { if (subtitle.language && !seenOriginal.has(subtitle.language)) { seenOriginal.add(subtitle.language); subtitles.push({ source: "yandex", format: "json", language: subtitle.language, url: subtitle.url }); } if (!subtitle.translatedLanguage) continue; subtitles.push({ source: "yandex", format: "json", language: subtitle.translatedLanguage, translatedFromLanguage: subtitle.language, url: subtitle.translatedUrl ?? subtitle.url }); } return subtitles; }; var SubtitlesProcessor = { processTokens(subtitles, descriptor) { const lines = []; for (const line of subtitles.subtitles) { const text = normalizeLineText(line); const tokens = buildLineTokens(line, descriptor, text); lines.push({ ...line, text, tokens }); } return lines; }, formatYoutubeSubtitles(subtitles, isAsr = false) { const events = subtitles.events ?? []; if (!events.length) { console.error("[VOT] Invalid YouTube subtitles format:", subtitles); return { format: "json", subtitles: [] }; } const processed = []; for (let i = 0; i < events.length; i += 1) { const event = events[i]; const segs = event.segs; if (!segs?.length) continue; const nextEvent = events[i + 1]; const durationMs = getYoutubeEventDurationMs(event, nextEvent); const { text, sourceTokens } = buildYoutubeSourceTokens(event, segs, durationMs); const normalizedText = text.trim(); if (!normalizedText) continue; processed.push({ text: normalizedText, startMs: event.tStartMs, durationMs, speakerId: "0", tokens: isAsr ? sourceTokens : [] }); } return { format: "json", subtitles: processed }; }, autoMerge(subtitles, descriptor) { if (!descriptor.isAutoGenerated) return subtitles; if (subtitles.subtitles.length < 2 || !subtitles.subtitles.some((line) => line.tokens.length > 0)) return subtitles; return { ...subtitles, subtitles: mergeAutoGeneratedSubtitleLines(subtitles.subtitles, descriptor.language) }; }, async fetchSubtitles(descriptorOrVideoData, requestLang, spokenLang) { let descriptor = parseSubtitleDescriptor(descriptorOrVideoData); if (!descriptor && isVideoDataForSubtitles(descriptorOrVideoData)) descriptor = pickDescriptorFromVideoData(descriptorOrVideoData, requestLang, spokenLang); if (!descriptor) return { format: "json", subtitles: [] }; const { source, format } = descriptor; let { url } = descriptor; if (source === "youtube") url = appendYoutubePoTokenParams(url); try { const subtitlesWithTokens = processFetchedSubtitles(normalizeFetchedSubtitles(await fetchRawSubtitles(url, format), descriptor), descriptor); debug.log("[VOT] Processed subtitles:", subtitlesWithTokens); return subtitlesWithTokens; } catch (error) { console.error("[VOT] Failed to process subtitles:", error); return { format: "json", subtitles: [] }; } }, async getSubtitles(client, videoData) { const { host, url, detectedLanguage: requestLang, videoId, duration, subtitles: extraSubtitles = [] } = videoData; try { const requestPayload = { videoData: { host, url, videoId, duration }, requestLang }; const res = await Promise.race([client.getSubtitles(requestPayload), new Promise((_, reject) => { setTimeout(() => reject(/* @__PURE__ */ new Error("Timeout")), 5e3); })]); debug.log("[VOT] Subtitles response:", res); if (res.waiting) console.error("[VOT] Failed to get Yandex subtitles"); return sortSubtitles([...buildYandexSubtitles(res), ...extraSubtitles], requestLang); } catch (error) { let message = "Error in getSubtitles function"; if (error instanceof Error && error.message === "Timeout") message = "Failed to get Yandex subtitles: timeout"; console.error(`[VOT] ${message}`, error); throw error; } } }; //#endregion //#region src/videoHandler/modules/subtitles.ts var subtitlesSelectionRequestVersion = /* @__PURE__ */ new WeakMap(); function getPreferredSubtitlesLanguage(handler) { const videoData = handler.videoData; return handler.getPreferredSubtitlesLanguage(videoData?.detectedLanguage, videoData?.responseLanguage) ?? videoData?.responseLanguage ?? handler.translateToLang; } function getCacheDetectedLanguage(handler) { const videoData = handler.videoData; const detectedLanguage = videoData?.detectedLanguage?.toLowerCase(); if (detectedLanguage && detectedLanguage !== "auto") return detectedLanguage; return videoData?.responseLanguage?.toLowerCase() ?? handler.translateToLang; } function getCurrentSubtitlesCacheKey(handler) { const videoData = handler.videoData; if (!videoData?.videoId) return null; const detectedLanguage = getCacheDetectedLanguage(handler); if (!detectedLanguage) return null; const subtitleLanguage = getPreferredSubtitlesLanguage(handler); if (!subtitleLanguage) return null; return handler.getSubtitlesCacheKey(videoData.videoId, detectedLanguage, subtitleLanguage); } function buildSubtitleDescriptorKey(descriptor) { return [ descriptor.source, descriptor.format, descriptor.language, descriptor.translatedFromLanguage ?? "", descriptor.isAutoGenerated ? "1" : "0", descriptor.url ].join("|"); } function dedupeSubtitles(subtitles) { const seen = /* @__PURE__ */ new Set(); const result = []; for (const descriptor of subtitles) { const key = buildSubtitleDescriptorKey(descriptor); if (seen.has(key)) continue; seen.add(key); result.push(descriptor); } return result; } function enrichYoutubeSubtitlesForPreference(handler, subtitleLanguage) { const videoData = handler.videoData; if (!videoData) throw new Error("Video data is required to load subtitles"); if (handler.site.host !== "youtube" || !subtitleLanguage) return videoData; const preferredYoutubeSubtitles = YoutubeHelper.getSubtitles(subtitleLanguage); if (!preferredYoutubeSubtitles.length) return videoData; return { ...videoData, subtitles: dedupeSubtitles([...Array.isArray(videoData.subtitles) ? videoData.subtitles : [], ...preferredYoutubeSubtitles]) }; } function nextSubtitlesSelectionRequestVersion(handler) { const nextVersion = (subtitlesSelectionRequestVersion.get(handler) ?? 0) + 1; subtitlesSelectionRequestVersion.set(handler, nextVersion); return nextVersion; } function isCurrentSubtitlesSelectionRequest(handler, requestVersion) { return subtitlesSelectionRequestVersion.get(handler) === requestVersion; } function clearSelectedSubtitles(handler, overlayView) { if (handler.hasSubtitlesWidget()) handler.subtitlesWidget?.setContent(null); overlayView.downloadSubtitlesButton.hidden = true; overlayView.syncSubtitlesButtonState(false); handler.yandexSubtitles = null; return handler; } async function changeSubtitlesLang(subs) { debug.log("[onchange] subtitles", subs); const requestVersion = nextSubtitlesSelectionRequestVersion(this); const overlayView = this.uiManager.votOverlayView; if (!overlayView?.subtitlesSelect || !overlayView.downloadSubtitlesButton) return this; overlayView.subtitlesSelect.setSelectedValue(subs); overlayView.syncSubtitlesButtonState(subs !== DISABLED_SUBTITLES_VALUE); if (subs === "disabled") return clearSelectedSubtitles(this, overlayView); const subtitlesIndex = parseSubtitlesOptionIndex(subs); if (subtitlesIndex == null) return clearSelectedSubtitles(this, overlayView); const descriptor = getSubtitleDescriptorAtIndex(this.subtitles, subtitlesIndex); if (!descriptor) return clearSelectedSubtitles(this, overlayView); let subtitlesObj = { ...descriptor }; const proxiedSubtitlesUrl = proxifyYandexSubtitlesUrl(subtitlesObj.url, { translateProxyEnabled: this.data?.translateProxyEnabled, proxyWorkerHost: this.data?.proxyWorkerHost }); if (proxiedSubtitlesUrl !== subtitlesObj.url) { subtitlesObj = { ...subtitlesObj, url: proxiedSubtitlesUrl }; debug.log(`[VOT] Subs proxied via ${subtitlesObj.url}`); } const fetchedSubtitles = await SubtitlesProcessor.fetchSubtitles(subtitlesObj); if (!isCurrentSubtitlesSelectionRequest(this, requestVersion)) return this; this.yandexSubtitles = fetchedSubtitles; this.getSubtitlesWidget().setContent(this.yandexSubtitles, subtitlesObj.language); overlayView.downloadSubtitlesButton.hidden = false; overlayView.syncSubtitlesButtonState(true); return this; } async function updateSubtitlesLangSelect() { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.subtitlesSelect) return; const updatedOptions = buildSubtitlesSelectOptions(getIndexedSubtitleDescriptors(this.subtitles)); overlayView.subtitlesSelect.updateItems(updatedOptions); await this.changeSubtitlesLang(DISABLED_SUBTITLES_VALUE); } async function ensureSubtitlesForCurrentLangPair() { const cacheKey = getCurrentSubtitlesCacheKey(this); if (!cacheKey) { if (this.subtitlesCacheKey !== null || this.subtitles.length > 0) { this.subtitles = []; this.subtitlesCacheKey = null; await this.updateSubtitlesLangSelect(); } return this; } if (this.subtitlesCacheKey === cacheKey) { const hasCachedSubtitles = this.cacheManager.getSubtitles(cacheKey) !== void 0; if (this.subtitles.length > 0 || hasCachedSubtitles) return this; } const cachedSubs = this.cacheManager.getSubtitles(cacheKey); if (cachedSubs !== void 0) { this.subtitles = Array.isArray(cachedSubs) ? cachedSubs : []; this.subtitlesCacheKey = cacheKey; await this.updateSubtitlesLangSelect(); return this; } await this.loadSubtitles(); return this; } /** * Hotkey/helper: enables subtitles for the currently selected language pair. * * If an exact "from -> to" subtitles track is unavailable, falls back to any * subtitles track in the target language. * For same-language pair (from == to), prefer site subtitles before Yandex. */ async function enableSubtitlesForCurrentLangPair() { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.subtitlesSelect) return this; try { await ensureSubtitlesForCurrentLangPair.call(this); } catch { return this; } const fromLang = this.videoData?.detectedLanguage ?? this.translateFromLang; const toLang = getPreferredSubtitlesLanguage(this); if (!toLang) return this; const bestIdx = pickBestSubtitlesIndex(getIndexedSubtitleDescriptors(this.subtitles), fromLang, toLang); if (bestIdx == null) return this; if (getSelectedSubtitlesValue(overlayView.subtitlesSelect.selectedValues) === String(bestIdx)) return this; await this.changeSubtitlesLang(String(bestIdx)); return this; } /** * Re-evaluates the active subtitles track for the currently selected language * pair, but only when auto-subtitles are enabled. */ async function refreshAutoSubtitlesForCurrentLangPair() { if (!this.data?.autoSubtitles || !this.videoData?.videoId) return this; await this.enableSubtitlesForCurrentLangPair(); return this; } /** * Hotkey helper: toggles subtitles. * * - If subtitles are currently enabled (any non-"disabled" value), disable them. * - If subtitles are disabled, enable the best subtitles for the current * language pair. */ async function toggleSubtitlesForCurrentLangPair() { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.subtitlesSelect) return this; const currentValue = getSelectedSubtitlesValue(overlayView.subtitlesSelect.selectedValues); if (currentValue && currentValue !== "disabled") { await this.changeSubtitlesLang(DISABLED_SUBTITLES_VALUE); return this; } await this.enableSubtitlesForCurrentLangPair(); return this; } async function loadSubtitles() { if (!this.videoData?.videoId) { console.error(`[VOT] ${localizationProvider.getDefault("VOTNoVideoIDFound")}`); this.subtitles = []; this.subtitlesCacheKey = null; return; } const subtitleLanguage = getPreferredSubtitlesLanguage(this); if (!subtitleLanguage) { this.subtitles = []; this.subtitlesCacheKey = null; await this.updateSubtitlesLangSelect(); return; } const cacheKey = this.getSubtitlesCacheKey(this.videoData.videoId, this.videoData.detectedLanguage, subtitleLanguage); try { let cachedSubs = this.cacheManager.getSubtitles(cacheKey); if (!cachedSubs) { let inflight = this.subtitlesLoadPromises.get(cacheKey); if (inflight === void 0) { const videoDataForSubtitles = enrichYoutubeSubtitlesForPreference(this, subtitleLanguage); inflight = SubtitlesProcessor.getSubtitles(this.votClient, videoDataForSubtitles); this.subtitlesLoadPromises.set(cacheKey, inflight); } try { cachedSubs = await inflight; cachedSubs = Array.isArray(cachedSubs) ? cachedSubs : []; this.cacheManager.setSubtitles(cacheKey, cachedSubs); } finally { if (this.subtitlesLoadPromises.get(cacheKey) === inflight) this.subtitlesLoadPromises.delete(cacheKey); } } this.subtitles = Array.isArray(cachedSubs) ? cachedSubs : []; this.subtitlesCacheKey = cacheKey; } catch (error) { console.error("[VOT] Failed to load subtitles:", error); this.subtitles = []; this.subtitlesCacheKey = null; } await this.updateSubtitlesLangSelect(); } //#endregion //#region src/videoHandler/volumeLink.ts function syncVideoLinkSnapshot(state, volumePercent) { state.lastVideoPercent = clampPercentInt(volumePercent); } function syncTranslationLinkSnapshot(state, volumePercent) { state.lastTranslationPercent = clampPercentInt(volumePercent); } function getSharedTranslationRange(translationMin, translationMax) { const min = clampPercentInt(translationMin); return { min, max: clampPercentInt(Math.min(100, translationMax), min, 100) }; } /** * Applies delta-based volume linking and mutates `state` in place. * * Rules: * - "video" initiator: translation changes by the same delta as video. * - "translation" initiator: video changes by the same delta as translation. * * This preserves relative offset between sliders (until clamped by bounds), * instead of forcing a 1:1 mirror. */ function applyVolumeLinkDelta({ state, fromType, newVolume, currentVideo, currentTranslation, translationMin, translationMax }) { const sharedTranslationRange = getSharedTranslationRange(translationMin, translationMax); if (!state.initialized) { state.lastVideoPercent = clampPercentInt(currentVideo); state.lastTranslationPercent = clampInt(Number(currentTranslation), sharedTranslationRange.min, sharedTranslationRange.max); state.initialized = true; } if (fromType === "video") { const normalizedVideo = clampPercentInt(newVolume); const delta = normalizedVideo - clampPercentInt(state.lastVideoPercent); state.lastVideoPercent = normalizedVideo; const nextTranslation = clampInt(state.lastTranslationPercent + delta, sharedTranslationRange.min, sharedTranslationRange.max); state.lastTranslationPercent = nextTranslation; return { nextTranslation }; } const normalizedTranslation = clampInt(Number.isFinite(newVolume) ? newVolume : currentTranslation, sharedTranslationRange.min, sharedTranslationRange.max); const delta = normalizedTranslation - state.lastTranslationPercent; state.lastTranslationPercent = normalizedTranslation; const nextVideo = clampPercentInt(state.lastVideoPercent + delta); state.lastVideoPercent = nextVideo; return { nextVideo }; } //#endregion //#region src/VideoHandler.ts var RESOLVED_VOID_PROMISE = Promise.resolve(); var TRANSLATION_LOADING_MESSAGES = /* @__PURE__ */ new Set([ "Подготавливаем перевод", "Видео передано в обработку", "Ожидаем перевод видео", "Загружаем переведенное аудио" ]); var VideoHandler = class { video; container; site; translateFromLang = "auto"; autoSourceLanguageOverrideVideoId; translateToLang = calculatedResLang; data; videoData; firstPlay = true; audioContext; votClient; audioPlayer; abortController; actionsAbortController; /** Increments whenever we reset/abort translation actions to invalidate stale async work */ actionsGeneration = 0; notifier = new Notifier(); cacheManager; votSessionStorage = new VOTSessionStorageCache(); /** * In-flight subtitles list requests, keyed by subtitles cache key. * * Prevents duplicate parallel requests when the subtitles hotkey is spammed * before the first request resolves. */ subtitlesLoadPromises = /* @__PURE__ */ new Map(); downloadTranslation = null; isRefreshingTranslation = false; autoRetry; votOpts; volumeOnStart; autoVolumeMutedOnStart; /** * syncVolume (link translation and video volume) runtime state. * We keep last-known slider values to apply deltas reliably. */ volumeLinkState = { initialized: false, lastVideoPercent: 0, lastTranslationPercent: 0 }; /** * Used to ignore our own programmatic video-volume updates when observing * external UIs (e.g. YouTube volume panel aria mutations). */ internalVideoVolumeSetAt = 0; internalVideoVolumeSetPercent = null; internalVideoVolumeSuppressionMs = 250; internalVideoVolumeSetHistory = []; internalVideoVolumeSetHistoryLimit = 48; smartVolumeDuckingInterval; smartVolumeDuckingBaseline; smartVolumeLastApplied; smartVolumeLastTickAt = 0; smartVolumeLastSoundAt = 0; smartVolumeRmsMissingSinceAt = null; /** Smoothed translated-track RMS envelope (0..1). */ smartVolumeRmsEnvelope = 0; /** * Internal speech gate state for Smart Auto-Volume ducking. * * This is a debounced/hysteresis-based boolean that tracks whether the * translated track is considered "audible" for the purpose of ducking. */ smartVolumeSpeechGateOpen = false; smartVolumeIsDucked = false; longWaitingResCount = 0; hadAsyncWait = false; subtitles = []; subtitlesCacheKey = null; subtitlesWidget; activeTranslation = null; /** * In-flight async teardown for translation/audio player cleanup. * New translation starts should wait for this to avoid clear/init races. */ stopTranslatePromise = null; interactionChecker; uiManager; overlayVisibility; overlayVisibilityTargetsAbortController; translationOrchestrator; lifecycleController; translationHandler; videoManager; yandexSubtitles = null; resizeObserver; syncVolumeObserver; initialized = false; /** * Cached overlay mount points (root/portal). Recomputed when container changes. * Avoids doing the same DOM/style walks multiple times per lifecycle update. */ mountCache; /** * In-memory cache for translated error strings (RU -> UI language). * This avoids repeated translation API calls during retry loops when the * same backend message is emitted multiple times. */ errorTranslationCache = /* @__PURE__ */ new Map(); /** * Fullscreen helper for proper ShadowDOM support */ fullscreenHelper; /** * Returns fullscreen root for overlay if the active fullscreen session belongs * to the current video/container. Otherwise returns null. * For Shadow DOM players (e.g., Reddit's shreddit-player), returns shadowRoot * to ensure UI is mounted inside the shadow tree, not in the light DOM. */ getFullscreenOverlayRoot() { return this.fullscreenHelper?.getOverlayRoot() ?? null; } getOverlayMountPoints(container = this.container) { const fullscreenRoot = this.getFullscreenOverlayRoot(); const { base, root, portalContainer, subtitlesMountContainer } = resolveOverlayMountTargets({ container, site: this.site, fullscreenRoot }); const cache = this.mountCache; if (cache?.container === container && cache.base === base && cache.subtitlesMountContainer === subtitlesMountContainer && cache.fullscreenRoot === fullscreenRoot && (cache.root.isConnected ?? document.documentElement.contains(cache.root))) return { root: cache.root, portalContainer: cache.portalContainer, subtitlesMountContainer: cache.subtitlesMountContainer, fullscreenRoot: cache.fullscreenRoot }; this.mountCache = { container, base, root, portalContainer, subtitlesMountContainer, fullscreenRoot }; return { root, portalContainer, subtitlesMountContainer, fullscreenRoot }; } getOverlayMount(container = this.container) { const { root, portalContainer, subtitlesMountContainer } = this.getOverlayMountPoints(container); return { root, portalContainer, subtitlesMountContainer }; } /** * Builds a stable cache key for translations. * * NOTE: Keep this in sync with CacheManager expectations. * @param {string} videoId * @param {string} from * @param {string} to */ getTranslationCacheKey(videoId, from, to, translationHelp) { const requestLangForApi = this.getRequestLangForTranslation(from, to); const useLivelyVoice = this.isLivelyVoiceAllowed(requestLangForApi, to) && this.data?.useLivelyVoice; const helpStr = translationHelp === void 0 || translationHelp === null ? "" : stableStringify(translationHelp); return `${videoId}_${requestLangForApi}_${to}_${useLivelyVoice}_${helpStr ? fnv1a32ToKeyPart(helpStr) : "0"}`; } /** * Builds a stable cache key for subtitles. * * Bugfix: subtitles cache key must match the key used by loadSubtitles(). * @param {string} videoId * @param {string} detectedLanguage * @param {string} subtitleLanguage */ getSubtitlesCacheKey(videoId, detectedLanguage, subtitleLanguage) { return `${videoId}_${detectedLanguage}_${subtitleLanguage}_${this.data?.useLivelyVoice !== false}`; } getPreferredSubtitlesLanguage(detectedLanguage = this.videoData?.detectedLanguage ?? "auto", responseLanguage = this.videoData?.responseLanguage ?? this.translateToLang, preference = this.data?.responseLanguageSubtitles) { return resolveSubtitlesLanguage(preference, detectedLanguage, responseLanguage) ?? responseLanguage ?? detectedLanguage; } isActionStale(actionContext) { if (!actionContext) return false; return this.actionsGeneration !== actionContext.gen || this.videoData?.videoId !== actionContext.videoId; } updateVOTClientRequestSignal() { if (!this.votClient) return; this.votClient.provider.fetchOpts = { ...this.votClient.provider.fetchOpts, signal: this.actionsAbortController.signal }; } resetActionsAbortController(reason) { try { this.actionsAbortController?.abort(reason); } catch {} this.actionsAbortController = new AbortController(); this.actionsGeneration++; this.updateVOTClientRequestSignal(); } /** * Constructs a new VideoHandler instance. * @param {HTMLVideoElement} video The video element to handle. * @param {HTMLElement} container The container element for the video. * @param {Object} site The site object associated with the video. */ constructor(video, container, site) { debug.log("[VideoHandler] add video:", video, "container:", container, this); this.video = video; this.container = container; this.site = site; this.abortController = new AbortController(); this.actionsAbortController = new AbortController(); this.cacheManager = new InMemoryCacheManager(); this.interactionChecker = createIntervalIdleChecker(); this.interactionChecker.start(); const mount = this.getOverlayMount(container); this.uiManager = new UIManager({ mount, data: this.data, videoHandler: this, intervalIdleChecker: this.interactionChecker }); this.overlayVisibility = new OverlayVisibilityController({ checker: this.interactionChecker, getOverlayView: () => this.uiManager.votOverlayView ?? null, getAutoHideDelay: () => this.getAutoHideDelay(), isInteractiveNode: (node) => this.isOverlayInteractiveNode(node) }); this.translationOrchestrator = new TranslationOrchestrator({ isFirstPlay: () => this.firstPlay, setFirstPlay: (next) => { this.firstPlay = next; }, isAutoTranslateEnabled: () => Boolean(this.data?.autoTranslate), getVideoId: () => this.videoData?.videoId, scheduleAutoTranslate: () => this.runAutoTranslate(), isMobileYouTubeMuted: () => this.site.host === "youtube" && this.site.additionalData === "mobile" && this.video.muted, setMuteWatcher: (callback) => { let done = false; const fireOnce = () => { if (done) return; done = true; this.video.removeEventListener("volumechange", onVolumeChange); callback(); }; const onVolumeChange = () => { if (!this.video.muted) fireOnce(); }; this.video.addEventListener("volumechange", onVolumeChange, { signal: this.abortController.signal }); queueMicrotask(() => { if (!this.video.muted) fireOnce(); }); } }); this.lifecycleController = new VideoLifecycleController(createVideoLifecycleHost(this, (value) => this.getOverlayMount(value))); this.translationHandler = new VOTTranslationHandler(this); this.videoManager = new VOTVideoManager(this); this.fullscreenHelper = new FullscreenHelper({ container: this.container, video: this.video }); this.fullscreenHelper.addFullscreenChangeListener(() => { this.refreshOverlayMount(); }); } /** * Lazily creates the subtitles widget. * @returns {SubtitlesWidget} */ getSubtitlesWidget() { if (!this.subtitlesWidget) { const { subtitlesMountContainer } = this.getOverlayMountPoints(); this.subtitlesWidget = new SubtitlesWidget(this.video, this.uiManager.votOverlayView?.root ?? subtitlesMountContainer, this.interactionChecker); this.applySavedSubtitlesWidgetSettings(this.subtitlesWidget); } return this.subtitlesWidget; } applySavedSubtitlesWidgetSettings(widget) { if (!this.data) return; widget.setSmartLayout(typeof this.data.subtitlesSmartLayout === "boolean" ? this.data.subtitlesSmartLayout : true); if (typeof this.data.subtitlesMaxLength === "number") widget.setMaxLength(this.data.subtitlesMaxLength); if (typeof this.data.highlightWords === "boolean") widget.setHighlightWords(this.data.highlightWords); if (typeof this.data.subtitlesFontSize === "number") widget.setFontSize(this.data.subtitlesFontSize); if (typeof this.data.subtitlesFontFamily === "string") widget.setFontFamily(this.data.subtitlesFontFamily); if (typeof this.data.subtitlesOpacity === "number") widget.setOpacity(this.data.subtitlesOpacity); } /** * Determines whether subtitles widget is initialized\. * @returns {boolean} */ hasSubtitlesWidget() { return Boolean(this.subtitlesWidget); } resetSubtitlesWidget() { if (this.hasSubtitlesWidget()) { this.subtitlesWidget?.release(); this.subtitlesWidget = void 0; } } /** * Root element for overlay UI (buttons/menu) so it remains clickable on players * that disable pointer events on inner layers. */ get uiRoot() { const root = this.getOverlayMountPoints().root; return root instanceof ShadowRoot ? root.host : root; } /** * Determines the DOM container used for overlay portals. * @returns {HTMLElement} */ get portalContainer() { return this.getOverlayMountPoints().portalContainer; } /** * Returns the container element for event listeners. * @returns {HTMLElement} The event container. */ getEventContainer() { if (!this.site.eventSelector) return this.container; return document.querySelector(this.site.eventSelector) ?? this.container; } /** * Run auto translate using orchestrator dependencies. */ async runAutoTranslate() { await this.videoManager.videoValidator(); await this.uiManager.handleTranslationBtnClick(); } /** * Lazily initializes and returns the AudioContext. * @returns {AudioContext | undefined} */ getAudioContext() { if (this.audioContext) return this.audioContext; if (!this.isAudioContextSupported) return void 0; try { this.audioContext = initAudioContext(); return this.audioContext; } catch (err) { console.warn("[VOT] Failed to init AudioContext, falling back:", err); return; } } get isAudioContextSupported() { return globalThis.AudioContext !== void 0 || globalThis.webkitAudioContext !== void 0; } /** * Determines if audio should be preferred. * @returns {boolean} True if audio is preferred. */ getPreferAudio() { if (!this.getAudioContext()) return true; if (!this.data) return true; if (!this.data.newAudioPlayer) return true; if (this.videoData?.isStream) return true; if (this.data.newAudioPlayer && !this.data.onlyBypassMediaCSP) return false; return !this.site.needBypassCSP; } /** * Creates the audio player. * @returns {VideoHandler} The VideoHandler instance. */ createPlayer() { const preferAudio = this.getPreferAudio(); const audioContext = this.getAudioContext(); debug.log("preferAudio:", preferAudio); this.audioPlayer = new Chaimu({ video: this.video, debug: Boolean(false), fetchFn: GM_fetch, fetchOpts: { timeout: 0 }, preferAudio }); if (preferAudio && audioContext) this.audioPlayer.audioContext = audioContext; return this; } /** * Returns true if a detected external volume update is very likely caused by * our own recent programmatic setVideoVolume call. */ isLikelyInternalVideoVolumeChange(observedPercent) { const now = Date.now(); const history = this.internalVideoVolumeSetHistory; if (history.length > 0) { const recentHistory = history.filter((entry) => now - entry.at <= entry.suppressMs); history.splice(0, history.length, ...recentHistory); return recentHistory.some((entry) => Math.abs(observedPercent - entry.percent) <= 1); } if (this.internalVideoVolumeSetPercent === null) return false; if (now - this.internalVideoVolumeSetAt > this.internalVideoVolumeSuppressionMs) return false; return Math.abs(observedPercent - this.internalVideoVolumeSetPercent) <= 1; } /** * Initializes the VideoHandler: loads settings, UI, video data, events, etc. * @returns {Promise} */ init() { return init.call(this); } /** * Initializes the VOT client. * @returns {VideoHandler} This instance. */ async initVOTClient() { const proxyClientEnabled = isProxyClientEnabled(this.data ?? {}); let transportHost = workerHost; if (this.data?.translateProxyEnabled === 1) transportHost = proxyWorkerHostMode1; else if (proxyClientEnabled) transportHost = resolveProxyWorkerHost(this.data?.proxyWorkerHost); this.votOpts = { fetchFn: GM_fetch, fetchOpts: { signal: this.actionsAbortController.signal, forceGmXhr: shouldForceProxyClientGmXhr({ ...this.data, gmXhrSupported: isSupportGMXhr }) }, apiToken: hasValidAccountToken(this.data?.account) ? this.data?.account?.token : void 0, host: transportHost, provider: proxyClientEnabled ? VOTNextWorkerProvider : YandexProvider }; this.votClient = new VOTClient(this.votOpts); this.votClient.provider.sessions = await this.votSessionStorage.restore(transportHost, this.votClient.provider.sessions); const originalGetSession = this.votClient.provider.getSession.bind(this.votClient.provider); this.votClient.provider.getSession = async (module) => { const session = await originalGetSession(module); await this.votSessionStorage.persist(transportHost, this.votClient.provider.sessions); return session; }; return this; } /** * Sets the translation button state and text. * @param {string} status The new status. * @param {string} text The text to display. * @returns {VideoHandler} This instance. */ transformBtn(status, text) { this.uiManager.transformBtn(status, text); return this; } /** * @returns {boolean} True if the extension audio player has active audio source */ hasActiveSource() { return !!this.audioPlayer?.player?.src; } /** * Initializes extra event listeners (resize, click outside, keydown, etc.). */ initExtraEvents() { return initExtraEvents.call(this); } /** * Recomputes overlay mount points and rebinds interaction targets. * * Used when fullscreen state changes without changing `this.container` * (common for players inside Shadow DOM). */ refreshOverlayMount() { this.mountCache = void 0; if (this.fullscreenHelper) { this.fullscreenHelper.updateContainer(this.container); this.fullscreenHelper.updateVideo(this.video); } const nextMount = this.getOverlayMount(this.container); const mountChanged = !isSameOverlayMount(this.uiManager.mount, nextMount); this.uiManager.updateMount(nextMount); if (!mountChanged) return; this.rebindOverlayVisibilityTargets(); } /** * Re-attach overlayVisibility listeners to the *current* overlay button/menu elements. * * The overlay UI gets recreated in some flows (e.g. menu language change), * so listeners that were attached to the old DOM nodes must be re-bound. */ rebindOverlayVisibilityTargets = rebindOverlayVisibilityTargets; /** * Called when the video can play. */ setCanPlay = () => this.lifecycleController.setCanPlay(); isOverlayInteractiveNode(node) { return isOverlayInteractiveNode.call(this, node); } /** * Schedules hiding the overlay button with guard checks for internal navigation. */ getAutoHideDelay() { return getAutoHideDelay.call(this); } /** * Changes subtitles language based on user selection. * @param {string} subs The subtitles selection value. */ changeSubtitlesLang = changeSubtitlesLang; /** * Updates the subtitles selection options. */ updateSubtitlesLangSelect = updateSubtitlesLangSelect; /** * Ensures the in-memory subtitles list matches the current language-pair cache key. */ ensureSubtitlesForCurrentLangPair = ensureSubtitlesForCurrentLangPair; /** * Loads subtitles for the current video. */ loadSubtitles = loadSubtitles; /** * Enables subtitles that match the currently selected language pair (from -> to). * * Used by the subtitles hotkey: prefers Yandex captions for the exact pair, * then falls back to any captions in the target language. */ enableSubtitlesForCurrentLangPair() { return enableSubtitlesForCurrentLangPair.call(this); } /** * Re-evaluates the active subtitles track for the current language pair, * but only when auto-subtitles are enabled. */ refreshAutoSubtitlesForCurrentLangPair() { return refreshAutoSubtitlesForCurrentLangPair.call(this); } /** * Toggles subtitles for the current video. * * - If subtitles are enabled, this disables them. * - If subtitles are disabled, this enables the best subtitles track for the * current language pair. */ toggleSubtitlesForCurrentLangPair() { return toggleSubtitlesForCurrentLangPair.call(this); } getRequestLangForTranslation(requestLang, responseLang) { if (this.data?.useLivelyVoice && requestLang === "auto" && responseLang === "ru") return "en"; return requestLang; } isLivelyVoiceAllowed(requestLang = this.videoData?.detectedLanguage ?? "auto", responseLang = this.videoData?.responseLanguage ?? this.translateToLang) { if (this.getRequestLangForTranslation(requestLang, responseLang) === "auto" || responseLang !== "ru") return false; return true; } /** * Gets the video volume. * @returns {number} The video volume (0.0 - 1.0). */ getVideoVolume = () => this.videoManager.getVideoVolume(); /** * Sets the video volume. * @param {number} volume A number between 0 and 1. * @returns {VideoHandler} This instance. */ setVideoVolume(volume, options = {}) { const snapped = snapVolume01(volume); const suppressSyncMs = typeof options.suppressSyncMs === "number" && Number.isFinite(options.suppressSyncMs) ? Math.max(0, options.suppressSyncMs) : this.internalVideoVolumeSuppressionMs; const now = Date.now(); const percent = volume01ToPercent(snapped); this.internalVideoVolumeSetAt = now; this.internalVideoVolumeSetPercent = percent; this.internalVideoVolumeSetHistory.push({ at: now, percent, suppressMs: suppressSyncMs }); this.internalVideoVolumeSetHistory.splice(0, Math.max(0, this.internalVideoVolumeSetHistory.length - this.internalVideoVolumeSetHistoryLimit)); this.videoManager.setVideoVolume(snapped, { preserveYoutubeVolumeStorage: options.preserveYoutubeVolumeStorage }); return this; } setVideoMuted(muted) { this.videoManager.setVideoMuted(muted); return this; } /** * Keeps internal syncVolume state aligned with observed/programmatic video-volume changes. */ onVideoVolumeSliderSynced(volumePercent) { const normalized = clampPercentInt(volumePercent); if (!this.volumeLinkState.initialized) { syncVideoLinkSnapshot(this.volumeLinkState, normalized); return; } if (this.data?.syncVolume && this.hasActiveSource() && !this.isLikelyInternalVideoVolumeChange(normalized)) return; syncVideoLinkSnapshot(this.volumeLinkState, normalized); } /** * Keeps internal translation-volume snapshot aligned when syncVolume is * temporarily disabled, so re-enabling link mode does not apply stale deltas. */ onTranslationVolumeSliderSynced(volumePercent) { syncTranslationLinkSnapshot(this.volumeLinkState, volumePercent); } /** * Re-seeds syncVolume baseline from current UI slider values. * Useful when toggling syncVolume on/off to avoid stale delta state. */ resetVolumeLinkState(videoPercent, translationPercent) { syncVideoLinkSnapshot(this.volumeLinkState, videoPercent); syncTranslationLinkSnapshot(this.volumeLinkState, translationPercent); this.volumeLinkState.initialized = true; } clearVolumeLinkState() { this.volumeLinkState.initialized = false; this.volumeLinkState.lastVideoPercent = 0; this.volumeLinkState.lastTranslationPercent = 0; } /** * Checks if the video is muted. * @returns {boolean} True if muted. */ isMuted = () => this.videoManager.isMuted(); /** * Syncs the video volume slider. */ syncVideoVolumeSlider = () => this.videoManager.syncVideoVolumeSlider(); /** * Sets language select menu values. * @param {string} from Source language. * @param {string} to Target language. */ setSelectMenuValues = (from, to) => { this.videoManager.setSelectMenuValues(from, to); }; /** * Keeps translation and video sliders linked (syncVolume option). * * The implementation is delta-based inside the shared 0..100 link range. * Translation booster values above 100 remain available only while link mode * is disabled. */ syncVolumeWrapper(fromType, newVolume) { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.isInitialized()) return; const videoSlider = overlayView.videoVolumeSlider; const translationSlider = overlayView.translationVolumeSlider; if (!videoSlider || !translationSlider) return; const result = applyVolumeLinkDelta({ state: this.volumeLinkState, fromType, newVolume, currentVideo: Number(videoSlider.value), currentTranslation: Number(translationSlider.value), translationMin: translationSlider.min, translationMax: translationSlider.max }); const { nextVideo, nextTranslation } = result; if (typeof nextTranslation === "number") { translationSlider.value = nextTranslation; return result; } if (typeof nextVideo === "number") { videoSlider.value = nextVideo; this.setVideoVolume(nextVideo / 100); } return result; } /** * Retrieves video data. * @returns {Promise} The video data object. */ getVideoData = () => this.videoManager.getVideoData(); /** * Validates the video. * @returns {Promise} True if valid. */ videoValidator = () => this.videoManager.videoValidator(); /** * Stops translation and resets UI elements. */ stopTranslate() { if (this.stopTranslatePromise !== null) return this.stopTranslatePromise; const cleanup = async () => { if (this.audioPlayer?.player) { try { this.audioPlayer.player.removeVideoEvents(); this.audioPlayer.player.src = ""; await this.audioPlayer.player.clear(); } catch (err) { debug.log("[stopTranslate] audioPlayer cleanup error", err); } debug.log("audioPlayer after stopTranslate", this.audioPlayer); } this.activeTranslation = null; const overlayView = this.uiManager.votOverlayView; if (overlayView) { for (const control of [ overlayView.videoVolumeSlider, overlayView.translationVolumeSlider, overlayView.downloadTranslationButton ]) if (control) control.hidden = true; } this.downloadTranslation = null; this.longWaitingResCount = 0; this.hadAsyncWait = false; this.translationHandler?.stopTranslationEtaCountdown(); this.transformBtn("none", localizationProvider.get("translateVideo")); debug.log(`Volume on start: ${this.volumeOnStart}`); const restoreVolume = typeof this.smartVolumeDuckingBaseline === "number" ? this.smartVolumeDuckingBaseline : this.volumeOnStart; stopSmartVolumeDucking(this, { restoreVolume }); this.volumeOnStart = void 0; this.autoVolumeMutedOnStart = void 0; if (this.autoRetry !== void 0) { clearTimeout(this.autoRetry); this.autoRetry = void 0; } this.resetActionsAbortController("stopTranslate"); }; const inFlight = cleanup().finally(() => { if (this.stopTranslatePromise === inFlight) this.stopTranslatePromise = null; }); this.stopTranslatePromise = inFlight; return inFlight; } waitForPendingStopTranslate() { return this.stopTranslatePromise ?? RESOLVED_VOID_PROMISE; } /** * Updates the translation error message on the UI. * @param {string|Error} errorMessage The error message. */ async updateTranslationErrorMsg(errorMessage, signal, options = {}) { if (signal?.aborted) return; const translationTake = localizationProvider.get("translationTake"); const lang = localizationProvider.lang; if (options.countLongWait !== false) { this.longWaitingResCount = errorMessage === localizationProvider.get("translationTakeAboutMinute") ? this.longWaitingResCount + 1 : 0; debug.log("longWaitingResCount", this.longWaitingResCount); if (this.longWaitingResCount > 5) errorMessage = new VOTLocalizedError("TranslationDelayed"); } debug.log("updateTranslationErrorMsg message", errorMessage); const resolvedMessage = await this.resolveTranslationErrorDisplayMessage(errorMessage, translationTake, lang, signal); if (signal?.aborted || resolvedMessage === null) return; this.transformBtn("error", resolvedMessage); if (signal?.aborted) return; if (TRANSLATION_LOADING_MESSAGES.has(errorMessage)) { if (this.uiManager.votOverlayView?.votButton) this.uiManager.votOverlayView.votButton.loading = true; } } async resolveTranslationErrorDisplayMessage(errorMessage, translationTake, lang, signal) { if (errorMessage?.name === "VOTLocalizedError") return errorMessage.localizedMessage; if (errorMessage instanceof Error) return errorMessage.message; if (!this.shouldTranslateErrorMessage(errorMessage, translationTake, lang)) return this.stringifyTranslationError(errorMessage); return await this.getTranslatedErrorMessage(errorMessage, lang, signal); } shouldTranslateErrorMessage(errorMessage, translationTake, lang) { return Boolean(this.data?.translateAPIErrors) && lang !== "ru" && !errorMessage?.includes(translationTake); } stringifyTranslationError(errorMessage) { return Array.isArray(errorMessage) ? errorMessage.join("\n") : String(errorMessage ?? ""); } async getTranslatedErrorMessage(errorMessage, lang, signal) { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.votButton) return null; const messageStr = Array.isArray(errorMessage) ? errorMessage.join(" ") : String(errorMessage); const cacheKey = `${lang}:${messageStr}`; const cached = this.errorTranslationCache.get(cacheKey); if (cached) return cached; overlayView.votButton.loading = true; const translatedMessage = await translate(messageStr, "ru", lang); if (signal?.aborted) return null; const translatedText = Array.isArray(translatedMessage) ? translatedMessage.join("\n") : String(translatedMessage); this.errorTranslationCache.set(cacheKey, translatedText); this.trimErrorTranslationCache(); return translatedText; } trimErrorTranslationCache() { if (this.errorTranslationCache.size <= 50) return; const oldestKey = this.errorTranslationCache.keys().next().value; if (oldestKey) this.errorTranslationCache.delete(oldestKey); } /** * Called after translation is updated. * @param {string} audioUrl The URL of the translation audio. */ afterUpdateTranslation(audioUrl) { const overlayView = this.uiManager.votOverlayView; if (!overlayView?.votButton) return; const isSuccess = overlayView.votButton.container.dataset.status === "success"; if (overlayView.videoVolumeSlider) overlayView.videoVolumeSlider.hidden = !this.data?.showVideoSlider || !isSuccess; if (overlayView.translationVolumeSlider) overlayView.translationVolumeSlider.hidden = !isSuccess; if (overlayView.videoVolumeSlider && overlayView.translationVolumeSlider) this.resetVolumeLinkState(Number(overlayView.videoVolumeSlider.value), Number(overlayView.translationVolumeSlider.value)); else this.volumeLinkState.initialized = false; if (this.videoData && !this.videoData.isStream) { if (overlayView.downloadTranslationButton) overlayView.downloadTranslationButton.hidden = false; this.downloadTranslation = { url: audioUrl, videoId: this.videoData.videoId }; } debug.log("afterUpdateTranslation downloadTranslation", this.downloadTranslation); this.syncTranslationPlaybackVolume(); if (this.data?.sendNotifyOnComplete && this.hadAsyncWait && isSuccess) { this.notifier.translationCompleted(globalThis.location.hostname); this.hadAsyncWait = false; } } /** * Keeps the historical async hook for audio URL preparation. * Playback errors are handled by the native audio player path. * @param {string} audioUrl The audio URL. * @returns {Promise} The prepared audio URL. */ validateAudioUrl(audioUrl, actionContext) { return validateAudioUrl.call(this, audioUrl, actionContext); } scheduleTranslationRefresh() { scheduleTranslationRefresh.call(this); } refreshTranslationAudio = refreshTranslationAudio; /** * Proxifies the audio URL if needed. * @param {string} audioUrl The original audio URL. * @returns {string} The proxified audio URL. */ proxifyAudio(audioUrl) { return proxifyAudio.call(this, audioUrl); } /** * Reverts a previously proxified audio URL back to the original Yandex S3 URL. * * This allows us to re-apply proxy settings when the proxy host/mode changes * without permanently "locking in" the old proxy host in the current player * src. */ unproxifyAudio(audioUrl) { return unproxifyAudio.call(this, audioUrl); } /** * Called when proxy-related settings are changed at runtime. * * - Clears in-memory caches so old failures/URLs don't persist. * - Cancels any in-flight translation work. * - Best-effort refreshes the active audio source so the new proxy host/mode * takes effect immediately. */ handleProxySettingsChanged = handleProxySettingsChanged; isMultiMethodS3(url) { return isMultiMethodS3.call(this, url); } /** * Updates the translation audio source. * @param {string} audioUrl The audio URL. */ updateTranslation = updateTranslation; syncTranslationPlaybackVolume() { return syncTranslationPlaybackVolume.call(this); } /** * Translates the video/audio. * @param {string} VIDEO_ID The video ID. * @param {boolean} isStream Whether the video is a stream. * @param {string} requestLang Source language. * @param {string} responseLang Target language. * @param {any} translationHelp Optional translation helper data. */ translateFunc(VIDEO_ID, isStream, requestLang, responseLang, translationHelp) { return translateFunc.call(this, VIDEO_ID, isStream, requestLang, responseLang, translationHelp); } /** * used for enable audio downloader on this hosts */ isYouTubeHosts() { return isYouTubeHosts.call(this); } /** * Configures audio settings such as volume. */ setupAudioSettings() { return setupAudioSettings.call(this); } applyManualVideoVolumeOverride(volume) { return applyManualVideoVolumeOverride.call(this, volume); } /** * Stops translation and synchronizes volume. */ stopTranslation = async () => { this.translationOrchestrator?.reset(); this.overlayVisibility?.cancel(); await this.stopTranslate(); this.syncVideoVolumeSlider(); }; /** * Handles video source change events. */ handleSrcChanged = () => this.lifecycleController.handleSrcChanged(); /** Rebinds an active handler when the page replaces its video element. */ async replaceVideo(video) { if (this.video === video) return; debug.log("[VideoHandler] replaceVideo", video); await this.audioPlayer.replaceVideo(video); this.abortController.abort(); this.releaseExtraEvents(); this.video = video; this.abortController = new AbortController(); this.fullscreenHelper?.updateVideo(video); this.resetSubtitlesWidget(); this.initExtraEvents(); } /** * Releases resources and removes event listeners. */ async release() { debug.log("[VideoHandler] release"); this.initialized = false; try { await this.stopTranslation(); } catch (err) { debug.log("[VideoHandler] stopTranslation failed during release", err); } this.lifecycleController?.teardown(); this.abortController?.abort(); this.abortController = new AbortController(); this.fullscreenHelper?.destroy(); this.fullscreenHelper = void 0; this.overlayVisibility?.release(); this.releaseExtraEvents(); if (this.hasSubtitlesWidget()) { this.subtitlesWidget?.release(); this.subtitlesWidget = void 0; } this.interactionChecker?.destroy(); this.uiManager.release(); } /** * Collects report information for bug reporting. * @returns {Object} Report info object. */ collectReportInfo() { const info = getEnvironmentInfo(); const detectedLanguage = this.videoData?.detectedLanguage ?? "unknown"; const responseLanguage = this.videoData?.responseLanguage ?? "unknown"; const additionalInfo = `
Autogenerated by VOT:
  • OS: ${info.os}
  • Browser: ${info.browser}
  • Loader: ${info.loader}
  • Script version: ${info.scriptVersion}
  • URL: ${info.url}
  • Lang: ${detectedLanguage} -> ${responseLanguage} (Lively voice: ${this.data?.useLivelyVoice ?? false} | Audio download: ${this.data?.useAudioDownload ?? false})
  • Player: ${this.data?.newAudioPlayer ? "New" : "Old"} (CSP only: ${this.data?.onlyBypassMediaCSP ?? false})
  • Proxying mode: ${this.data?.translateProxyEnabled ?? 0}
`; return { assignees: "ilyhalight", template: `1-bug-report-${localizationProvider.lang === "ru" ? "ru" : "en"}.yml`, os: info.os, "script-version": info.scriptVersion, "additional-info": additionalInfo }; } /** * Releases extra event listeners. */ releaseExtraEvents = releaseExtraEvents; }; var videoObserver = new VideoObserver(createIntervalIdleChecker()); var videosWrappers = /* @__PURE__ */ new WeakMap(); var servicesCache = null; var bootState = getOrCreateBootState(); function getFrameContext() { return { frame: isIframe() ? "iframe" : "top", host: globalThis.location.hostname || "unknown", path: globalThis.location.pathname || "/" }; } function logBootstrap(message, details) { const ctx = getFrameContext(); const payload = { host: ctx.host, path: ctx.path, ...details }; debug.log(`[VOT][bootstrap][${ctx.frame}] ${message}`, payload); } function getServicesCached() { servicesCache ??= getService(); return servicesCache; } /** * Recursively finds the closest parent element matching a selector. * @param {SiteData} site The site data. * @param {HTMLElement} video The video element. * @returns {HTMLElement|null} The matching parent element. */ function findContainer(site, video) { debug.log("findContainer", site, site.selector, video); if (!site.selector) { debug.log("findContainer without selector, using parentElement"); return video.parentElement; } const matched = findConnectedContainerBySelector(video, site.selector); if (site.shadowRoot) debug.log("findContainer with site.shadowRoot", matched); else debug.log("findContainer without shadowRoot", matched); return matched; } /** * Main function to start the extension. */ async function main() { const bootstrapMode = resolveBootstrapMode({ isIframe: isIframe(), href: String(globalThis.location.href || ""), origin: globalThis.location.origin, authOrigin: authServerUrl }); if (globalThis.location.hostname === "drive.google.com") GM_addStyle(` section[data-fullscreen-control-supported="true"] { pointer-events: none !important; } section[data-fullscreen-control-supported="true"] > div[data-volume-slider-control-supported="true"], section[data-fullscreen-control-supported="true"] > div[data-playback-rate-setting-supported="true"] { pointer-events: auto !important; } `); if (bootstrapMode === "skip") { logBootstrap("Skipping bootstrap for non-runnable iframe"); return; } initIframeInteractor(); logBootstrap("Loading extension", { mode: bootstrapMode }); if (bootstrapMode === "auth-eager") await ensureRuntimeActivated("auth-page", logBootstrap); else logBootstrap("Lazy bootstrap enabled; waiting for video detection"); bindObserverListeners({ videoObserver, videosWrappers, ensureRuntimeActivated: (reason) => ensureRuntimeActivated(reason, logBootstrap), getServicesCached, findContainer, createVideoHandler: (video, container, site) => new VideoHandler(video, container, site) }); videoObserver.enable(); } async function bootstrapContentScript() { if (bootState.status === "booting" || bootState.status === "booted") { logBootstrap("bootstrap already initialized, skipping duplicate run", { status: bootState.status }); return; } bootState.status = "booting"; try { await main(); bootState.status = "booted"; } catch (e) { bootState.status = "failed"; bootState.error = e; console.error("[VOT]", e); } } (async () => { await bootstrapContentScript(); })(); //#endregion exports.bootstrapContentScript = bootstrapContentScript; return exports; })({});