// ==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 ? "" : "")), 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