Améliore Chaturbate en ajoutant de nombreuses nouvelles fonctionnalités : visionneuse multicaméra, rembobinage du flux, aperçus du flux, enregistreur de flux, commandes vidéo, programmes en ligne, liste de blocage et bien plus encore !
// ==UserScript== // @name Chaturbate Enhancer // @name:de Chaturbate Enhancer // @name:es Chaturbate Enhancer // @name:es-CO Chaturbate Enhancer // @name:it Chaturbate Enhancer // @name:fr Chaturbate Enhancer // @name:fr-CA Chaturbate Enhancer // @name:ru Chaturbate Enhancer // @name:tr Chaturbate Enhancer // @name:ro Chaturbate Enhancer // @name:no Chaturbate Enhancer // @name:nl Chaturbate Enhancer // @name:pl Chaturbate Enhancer // @name:ja Chaturbate Enhancer // @name:el Chaturbate Enhancer // @name:hu Chaturbate Enhancer // @name:fi Chaturbate Enhancer // @name:ar Chaturbate Enhancer // @name:hi Chaturbate Enhancer // @name:id Chaturbate Enhancer // @name:ko Chaturbate Enhancer // @name:pt-PT Chaturbate Enhancer // @name:pt-BR Chaturbate Enhancer // @name:zh Chaturbate Enhancer // @name:zh-CN Chaturbate Enhancer // @name:zh-TW Chaturbate Enhancer // @name:cs Chaturbate Enhancer // @name:sk Chaturbate Enhancer // @name:sl Chaturbate Enhancer // @name:sv Chaturbate Enhancer // @name:sr Chaturbate Enhancer // @name:af Chaturbate Enhancer // @name:sq Chaturbate Enhancer // @name:hy Chaturbate Enhancer // @name:be Chaturbate Enhancer // @name:bg Chaturbate Enhancer // @name:da Chaturbate Enhancer // @name:et Chaturbate Enhancer // @name:he Chaturbate Enhancer // @name:hr Chaturbate Enhancer // @name:fa Chaturbate Enhancer // @name:ur Chaturbate Enhancer // @name:bn Chaturbate Enhancer // @name:th Chaturbate Enhancer // @name:eo Chaturbate Enhancer // @name:ug Chaturbate Enhancer // @name:vi Chaturbate Enhancer // @description Enhances Chaturbate by adding multiple new features: multi-cam viewer, stream rewind, stream previews, stream recorder, video controls, online schedules, blocklist and much more! // @description:de Verbessert Chaturbate durch zahlreiche neue Funktionen: Multi-Cam-Viewer, Stream-Rewind, Stream-Vorschauen, Stream-Recorder, Videosteuerung, Online-Zeitpläne, Blockliste und vieles mehr! // @description:es Mejora Chaturbate añadiendo múltiples funciones nuevas: visor multicámara, rebobinado de la transmisión, previsualizaciones de la transmisión, grabador de transmisiones, controles de vídeo, horarios en línea, lista de bloqueo y mucho más. // @description:es-CO Mejora Chaturbate añadiendo múltiples funciones nuevas: visor multicámara, rebobinado de la transmisión, previsualizaciones de la transmisión, grabador de transmisiones, controles de vídeo, horarios en línea, lista de bloqueo y mucho más. // @description:it Migliora Chaturbate aggiungendo numerose nuove funzionalità: visualizzatore multi-camera, riavvolgimento dello streaming, anteprime dello streaming, registrazione dello streaming, controlli video, programmazione online, lista di blocco e molto altro! // @description:fr Améliore Chaturbate en ajoutant de nombreuses nouvelles fonctionnalités : visionneuse multicaméra, rembobinage du flux, aperçus du flux, enregistreur de flux, commandes vidéo, programmes en ligne, liste de blocage et bien plus encore ! // @description:fr-CA Améliore Chaturbate en ajoutant de nombreuses nouvelles fonctionnalités : visionneuse multicaméra, rembobinage du flux, aperçus du flux, enregistreur de flux, commandes vidéo, programmes en ligne, liste de blocage et bien plus encore ! // @description:ru Улучшает Chaturbate, добавляя множество новых функций: просмотр с нескольких камер, перемотка трансляции, предварительный просмотр трансляции, запись трансляции, управление видео, расписание онлайн-трансляций, черный список и многое другое! // @description:tr Chaturbate'e birçok yeni özellik ekleyerek onu geliştirir: çoklu kamera görüntüleyici, yayın geri sarma, yayın önizlemeleri, yayın kaydedici, video kontrolleri, çevrimiçi programlar, engelleme listesi ve daha fazlası! // @description:ro Îmbunătățește Chaturbate prin adăugarea mai multor funcții noi: vizualizare multi-cam, derulare înapoi a fluxului, previzualizări ale fluxului, înregistrare a fluxului, comenzi video, programări online, listă de blocare și multe altele! // @description:no Forbedrer Chaturbate ved å legge til flere nye funksjoner: flerkameravisning, tilbakespoling av strømming, forhåndsvisning av strømming, strømmingsopptaker, videokontroller, online-planer, blokkeringsliste og mye mer! // @description:nl Verbetert Chaturbate met meerdere nieuwe functies: multicam-viewer, stream terugspoelen, streamvoorbeelden, streamrecorder, videobediening, online schema's, blokkeerlijst en nog veel meer! // @description:pl Udoskonala Chaturbate, dodając wiele nowych funkcji: przeglądarkę wielu kamer, przewijanie strumienia, podglądy strumienia, nagrywanie strumienia, sterowanie wideo, harmonogramy online, listę blokowanych i wiele więcej! // @description:ja Chaturbateに多数の新機能を追加して機能を強化します:マルチカムビューアー、ストリーム巻き戻し、ストリームプレビュー、ストリームレコーダー、ビデオコントロール、オンラインスケジュール、ブロックリストなど! // @description:el Βελτιώνει το Chaturbate προσθέτοντας πολλές νέες λειτουργίες: προβολή πολλαπλών καμερών, επαναφορά ροής, προεπισκοπήσεις ροής, εγγραφή ροής, στοιχεία ελέγχου βίντεο, online χρονοδιαγράμματα, λίστα αποκλεισμού και πολλά άλλα! // @description:hu Több új funkció hozzáadásával fejleszti a Chaturbate-et: többkamerás néző, stream visszatekerés, stream előnézetek, stream felvevő, videovezérlők, online ütemtervek, tiltólista és még sok más! // @description:fi Parantaa Chaturbatea lisäämällä useita uusia ominaisuuksia: usean kameran katseluohjelman, suoratoiston kelauksen taaksepäin, suoratoiston esikatselun, suoratoiston tallennuksen, videosäätimet, online-aikataulut, estolistan ja paljon muuta! // @description:ar يُحسّن موقع Chaturbate بإضافة العديد من الميزات الجديدة: عارض الكاميرات المتعددة، وإعادة تشغيل البث، ومعاينات البث، ومسجل البث، وأدوات التحكم في الفيديو، والجداول الزمنية عبر الإنترنت، وقائمة الحظر، وغير ذلك الكثير! // @description:hi Chaturbate में कई नई सुविधाएं जोड़कर इसे और बेहतर बनाया गया है: मल्टी-कैम व्यूअर, स्ट्रीम रिवाइंड, स्ट्रीम प्रीव्यू, स्ट्रीम रिकॉर्डर, वीडियो कंट्रोल, ऑनलाइन शेड्यूल, ब्लॉकलिस्ट और भी बहुत कुछ! // @description:id Meningkatkan Chaturbate dengan menambahkan berbagai fitur baru: penampil multi-kamera, putar ulang siaran, pratinjau siaran, perekam siaran, kontrol video, jadwal online, daftar blokir, dan masih banyak lagi! // @description:ko Chaturbate에 멀티캠 뷰어, 스트림 되감기, 스트림 미리보기, 스트림 녹화, 비디오 제어, 온라인 일정 관리, 차단 목록 등 다양한 신규 기능을 추가하여 더욱 향상시켰습니다! // @description:pt-PT Aprimora o Chaturbate adicionando diversos novos recursos: visualizador multicâmera, retrocesso da transmissão, pré-visualizações da transmissão, gravador de transmissão, controles de vídeo, agendamento online, lista de bloqueio e muito mais! // @description:pt-BR Aprimora o Chaturbate adicionando diversos novos recursos: visualizador multicâmera, retrocesso da transmissão, pré-visualizações da transmissão, gravador de transmissão, controles de vídeo, agendamento online, lista de bloqueio e muito mais! // @description:zh Chaturbate 新增多项功能:多摄像头查看器、直播回放、直播预览、直播录制、视频控制、在线日程安排、屏蔽列表等等! // @description:zh-CN Chaturbate 新增多项功能:多摄像头查看器、直播回放、直播预览、直播录制、视频控制、在线日程安排、屏蔽列表等等! // @description:zh-TW Chaturbate 新增多项功能:多摄像头查看器、直播回放、直播预览、直播录制、视频控制、在线日程安排、屏蔽列表等等! // @description:cs Vylepšuje Chaturbate přidáním několika nových funkcí: prohlížeč z více kamer, přetáčení streamu, náhledy streamu, nahrávání streamu, ovládání videa, online plány, seznam blokovaných a mnoho dalšího! // @description:sk Vylepšuje Chaturbate pridaním viacerých nových funkcií: prehliadač viacerých kamier, pretočenie streamu, ukážky streamu, nahrávanie streamu, ovládanie videa, online plány, zoznam blokovaných a oveľa viac! // @description:sl Izboljša Chaturbate z dodajanjem več novih funkcij: pregledovalnik več kamer, previjanje pretoka nazaj, predogledi pretoka, snemalnik pretoka, video nadzor, spletni urniki, seznam blokiranih in še veliko več! // @description:sv Förbättrar Chaturbate genom att lägga till flera nya funktioner: multikameravisning, återspolning av stream, förhandsvisning av stream, streaminspelare, videokontroller, onlinescheman, blockeringslista och mycket mer! // @description:sr Побољшава Chaturbate додавањем више нових функција: прегледач више камера, премотавање стрима, прегледи стрима, снимач стрима, контроле видеа, онлајн распореди, листа блокираних и још много тога! // @description:af Verbeter Chaturbate deur verskeie nuwe funksies by te voeg: multi-kamera-kyker, stroomterugspoel, stroomvoorskoue, stroomopnemer, videokontroles, aanlyn skedules, blokkeringslys en nog baie meer! // @description:sq Përmirëson Chaturbate duke shtuar shumë veçori të reja: shikues me shumë kamera, rikthim transmetimi, parapamje transmetimi, regjistrues transmetimi, kontrolle videoje, orare online, listë bllokimi dhe shumë më tepër! // @description:hy Բարելավում է Chaturbate-ը՝ ավելացնելով մի քանի նոր հնարավորություններ՝ բազմախցիկ դիտում, հոսքի հետադարձում, հոսքի նախադիտում, հոսքի ձայնագրիչ, տեսանյութերի կառավարման տարրեր, առցանց ժամանակացույցեր, արգելափակումների ցանկ և շատ ավելին: // @description:be Паляпшае Chaturbate, дадаўшы некалькі новых функцый: прагляд з некалькіх камер, перамотку трансляцыі, папярэдні прагляд трансляцыі, запіс трансляцыі, кіраванне відэа, онлайн-расклады, спіс блакіроўкі і многае іншае! // @description:bg Подобрява Chaturbate, като добавя множество нови функции: преглед на няколко камери, превъртане назад на потока, преглед на потока, запис на поток, видео контроли, онлайн графици, списък с блокирани и много други! // @description:da Forbedrer Chaturbate ved at tilføje flere nye funktioner: multi-cam viewer, stream rewind, stream previews, stream recorder, video controls, online skemaer, blokeringsliste og meget mere! // @description:et Täiustab Chaturbate'i mitme uue funktsiooni lisamisega: mitme kaameraga vaataja, voogesituse tagasikerimine, voogesituse eelvaated, voogesituse salvestaja, video juhtnupud, veebipõhised ajakavad, blokeeritud nimekirjad ja palju muud! // @description:he משפר את Chaturbate על ידי הוספת מספר תכונות חדשות: צפייה מרובה מצלמות, הרצה אחורה של שידורים, תצוגות מקדימות של שידורים, מקליט שידורים, בקרות וידאו, לוחות זמנים מקוונים, רשימת חסימות ועוד! // @description:hr Poboljšava Chaturbate dodavanjem više novih značajki: preglednik više kamera, premotavanje streama, pregledi streama, snimač streama, video kontrole, online rasporedi, popis blokiranih i još mnogo toga! // @description:fa Chaturbate با اضافه کردن چندین ویژگی جدید، بهبود یافته است: نمایشگر چند دوربینه، عقب بردن پخش زنده، پیشنمایش پخش زنده، ضبط پخش زنده، کنترل ویدیو، زمانبندی آنلاین، لیست سیاه و موارد دیگر! // @description:ur متعدد نئی خصوصیات شامل کرکے چیٹربیٹ کو بہتر بناتا ہے: ملٹی کیم ویور، اسٹریم ریوائنڈ، اسٹریم پریویو، اسٹریم ریکارڈر، ویڈیو کنٹرول، آن لائن شیڈول، بلاک لسٹ اور بہت کچھ! // @description:bn একাধিক নতুন ফিচার যোগ করে চ্যাটুরবেটকে আরও উন্নত করা হয়েছে: মাল্টি-ক্যাম ভিউয়ার, স্ট্রিম রিওয়াইন্ড, স্ট্রিম প্রিভিউ, স্ট্রিম রেকর্ডার, ভিডিও কন্ট্রোল, অনলাইন শিডিউল, ব্লক লিস্ট এবং আরও অনেক কিছু! // @description:th ปรับปรุง Chaturbate ด้วยการเพิ่มฟีเจอร์ใหม่มากมาย เช่น การดูหลายกล้อง การย้อนดูสตรีม การดูตัวอย่างสตรีม การบันทึกสตรีม การควบคุมวิดีโอ ตารางเวลาออนไลน์ รายชื่อผู้ถูกบล็อก และอื่นๆ อีกมากมาย! // @description:eo Plibonigas Chaturbate aldonante plurajn novajn funkciojn: plurkameraa spektanto, rebobenado de la fluo, antaŭrigardoj de la fluo, fluoregistrilo, videokontroliloj, retaj horaroj, bloklisto kaj multe pli! // @description:ug Chaturbate نى كۆپ خىل يېڭى ئىقتىدارلارنى قوشۇش ئارقىلىق كۈچەيتىدۇ: كۆپ كامېرالىق كۆرۈش، ئېقىمنى ئارقىغا ئايلاندۇرۇش، ئېقىمنى ئالدىن كۆرۈش، ئېقىم خاتىرىلىگۈچ، سىن كونتروللىرى، تور ۋاقىت جەدۋىلى، توسۇش تىزىملىكى ۋە باشقا نۇرغۇن ئىقتىدارلار! // @description:vi Nâng cấp Chaturbate bằng cách thêm nhiều tính năng mới: trình xem nhiều camera, tua lại video, xem trước video, ghi video, điều khiển video, lên lịch trực tuyến, danh sách chặn và nhiều hơn nữa! // @version 8.9.8 // @author improper.dev // @license CC-BY-ND-4.0; https://creativecommons.org/licenses/by-nd/4.0/legalcode // @copyright improper.dev (https://improper.dev/) // @namespace https://improper.dev/ // @homepage https://cb-enh.improper.dev/ // @supportURL https://sleazyfork.org/en/scripts/441079-chaturbate-enhancer/feedback // @contributionURL https://cb-enh.improper.dev/contribute // @icon https://www.google.com/s2/favicons?sz=32&domain=chaturbate.com // @icon64 https://www.google.com/s2/favicons?sz=64&domain=chaturbate.com // @match https://chaturbate.com/* // @match https://*.chaturbate.com/* // @exclude https://chaturbate.com/security/* // @exclude https://*.chaturbate.com/security/* // @exclude https://secure.chaturbate.com/* // @connect cb-enh.improper.dev // @connect cb-enh-api.improper.dev // @connect cb-enh-api2.improper.dev // @connect cb-enh-thumb.improper.dev // @connect jpeg.live.mmcdn.com // @connect thumb.live.mmcdn.com // @grant GM.addElement // @grant GM.xmlHttpRequest // @grant GM.registerMenuCommand // @grant GM.unregisterMenuCommand // @grant GM.setClipboard // @grant GM.download // @grant GM.info // @require https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js // @run-at document-start // @antifeature referral-link // ==/UserScript== /* Chaturbate™ Enhancer v8.9.8 © improper.dev Please do not share modified version of this script without permission. We have spent significant time and effort on developing this script. Instead, you are welcome to contribute to the main script code. You can submit code patches, bug reports and ideas at https://sleazyfork.org/en/scripts/441079-chaturbate-enhancer/feedback or via e-mail: [email protected] You will be credited for your contributions. Thank you! */ /* Attribution-NoDerivatives 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-NoDerivatives 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. c. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. d. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. e. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. f. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. g. Licensor means the individual(s) or entity(ies) granting rights under this Public License. h. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. i. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. j. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce and reproduce, but not Share, Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material, You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database, provided You do not Share Adapted Material; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. */ (function() { 'use strict'; let gIsFullVideoPage = false; if(document.location.pathname === '/fullvideo/') { const params = new URLSearchParams(window?.location?.search); if(params && params.get('b') && params.get('i') === '') { gIsFullVideoPage = true; } } let gFunctionsToRun = []; let gFunctionsToAdd = []; let gStylesToApply = []; function addStyle(style) { if(!document.documentElement) { gStylesToApply.push(style); return; } const elStyle = document.createElement('style'); elStyle.textContent = style; document.documentElement.appendChild(elStyle); return elStyle; } function addElement(parentNode, tagName, attributes) { if(typeof GM !== 'undefined' && typeof GM.addElement !== 'undefined') { return GM.addElement(parentNode, tagName, attributes) } } function addElementFallback(parentNode, tagName, attributes) { if(typeof GM !== 'undefined' && typeof GM.addElement !== 'undefined') { return GM.addElement(parentNode, tagName, attributes) } const el = document.createElement(tagName); if(attributes) { for(const key in attributes) { if(Object.prototype.hasOwnProperty.call(attributes, key)) { if(key === 'textContent' || key === 'innerText') { el[key] = attributes[key]; } else if(key.startsWith('on') && typeof attributes[key] === 'function') { el.addEventListener(key.slice(2), attributes[key]); } else { el.setAttribute(key, attributes[key]); } } } } parentNode.appendChild(el); return el; } function xmlhttpRequest(details) { let func = null; if(typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') { func = GM.xmlHttpRequest; } if(!func) { return; } return func(details); } function xmlhttpRequest(details) { let func = null; if( typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function' ) { func = GM.xmlHttpRequest; } else if( typeof GM_xmlhttpRequest === 'function' ) { func = GM_xmlhttpRequest; } if(!func) { throw new Error( 'GM.xmlHttpRequest is unavailable' ); } return func(details); } async function xmlhttpRequestArrayBuffer(url) { return new Promise( (resolve, reject) => { let request; try { request = xmlhttpRequest_tryNative({ method: 'GET', url, responseType: 'arraybuffer', onload: response => { if( response.status < 200 || response.status >= 300 ) { reject( new Error( `HTTP ${response.status} while fetching ${url}` ) ); return; } if(!response.response) { reject( new Error( `Empty response while fetching ${url}` ) ); return; } resolve( response.response ); }, onerror: error => { reject( new Error( `GM request failed: ${url}` ) ); }, onabort: () => { reject( new DOMException( 'GM request aborted', 'AbortError' ) ); }, ontimeout: () => { reject( new DOMException( 'GM request timed out', 'TimeoutError' ) ); } }); } catch(error) { reject(error); } if( request && typeof request.catch === 'function' ) { request.catch(reject); } } ); } function xmlhttpRequest_tryNative(details) { if(gDoNeedAlternativeWindowApproach) { xmlhttpRequest(details); return null; } const xhr = new XMLHttpRequest(); xhr.open(details.method || 'GET', details.url, true); if(details.timeout) { xhr.timeout = details.timeout; } if(details.responseType) { xhr.responseType = details.responseType; } if(details.headers) { for(const key in details.headers) { if(Object.prototype.hasOwnProperty.call(details.headers, key)) { xhr.setRequestHeader(key, details.headers[key]); } } } xhr.onload = function() { if(typeof details.onload === 'function') { const res = { responseText: null, status: xhr.status, response: xhr.response, xhr: xhr }; if(xhr.responseType === '' || xhr.responseType === 'text') { res.responseText = xhr.responseText; } details.onload(res); } }; xhr.onerror = function() { if(typeof details.onerror === 'function') { details.onerror({ xhr: xhr }); } }; xhr.ontimeout = function() { if(typeof details.ontimeout === 'function') { details.ontimeout({ xhr: xhr }); } }; xhr.onabort = function() { if(typeof details.onabort === 'function') { details.onabort({ xhr: xhr }); } }; xhr.send(details.data || null); return xhr; } function abortRequest(req) { if(!req) { return false; } if(typeof req !== 'object') { return false; } if(typeof req.abort !== 'function') { return false; } try { req.abort(); } catch(e) { return false; } return true; } function _getJSON(xhr_func, url, callback, an = false) { const details = { method: 'GET', url: url, timeout: 10000, anonymous: an, onload: function(req) { let data; try { data = JSON.parse(req.responseText); } catch(e) { return; } callback(data); } }; xhr_func(details); } function getJSON(url, callback) { _getJSON(xmlhttpRequest_tryNative, url, callback); } function getJSON_noNative(url, callback, an = false) { _getJSON(xmlhttpRequest, url, callback, an); } function registerMenuCommand(name, callback, accessKey) { if(typeof GM !== 'undefined' && typeof GM.registerMenuCommand !== 'undefined') { return GM.registerMenuCommand(name, callback, accessKey); } } function unregisterMenuCommand(menuCmdId) { if(typeof GM !== 'undefined' && typeof GM.unregisterMenuCommand !== 'undefined') { return GM.unregisterMenuCommand(menuCmdId); } } function setClipboard(data, info) { if(typeof GM !== 'undefined' && typeof GM.setClipboard !== 'undefined') { GM.setClipboard(data, info); return true; } return false; } const acre3 = 'ePs287p8MVIhXZizfUzY'; function download(data) { if(typeof GM !== 'undefined' && typeof GM.download !== 'undefined') { GM.download(data); return; } const elLink = document.createElement('a'); elLink.download = data['name']; elLink.href = data['url']; elLink.click(); if('onloadend' in data) { data['onloadend'](); } } function openInNewTab(url, rel = null) { const elLink = document.createElement('a'); elLink.target = '_blank'; if(rel) { elLink.rel = rel; } elLink.href = url; elLink.click(); } const gVersion = '8.9.8'; let gIntvWaitVideo = null; let gIntvUpdateVideoTime = null; let gIntvWaitMoreRooms = null; let gIntvAcceptRules = null; let gIntvAddVideoControlsBtn = null; let gIntvWaitBuyBox = null; let gIntvWaitBuyBox2 = null; let gIntvUpdateFollowedList = null; let gIntvCheckIfRoomIsOnline = null; let gIntvCheckIfRoomIsOnline_2 = null; let gIntvPatchVideo = null; let gIntvPatchHLS = null; let gIntvScheduleUpdateCurrentHour = null; let gIntvWaitForPlayBtn = null; let gIntvWaitBioTable = null; let gIntvFollowedDropDownUIUpdate = null; const SETTINGS_SCHEMA = { 'animate_thumbnails': 'boolean', 'grid-size': 'int', 'disable-big-previews': 'boolean', 'mute-big-previews': 'boolean', 'hide-vid-logo': 'boolean', 'rewind': 'boolean', 'smargins': 'boolean', 'inactive-quality': 'boolean', 'error-quality': 'boolean', 'no-socials': 'boolean', 'no-merch-links': 'boolean', 'no-surveys': 'boolean', 'auto-chat-rules': 'boolean', 'chat-hide-subject-notices': 'boolean', 'rec-exit-warn': 'boolean', 'vm': 'boolean', 'chat-hide-tips': 'boolean', 'inactive-load': 'boolean', 'rec-no-audio': 'boolean', 'rec-no-video': 'boolean', 'multi-unmute-hover': 'boolean', 'multi-align-right': 'boolean', 'multi-auto-remove': 'boolean', 'multi-minifscreen': 'boolean', 'multi-max-quality': 'int', 'blur-mute': 'boolean', 'duration': 'boolean', 'chat-hide-notices': 'boolean', 'chat-hide-greys': 'boolean', 'open-new-tab': 'boolean', 'multi-auto-hide-priv': 'boolean', 'multi-show-subject': 'boolean', 'grid-more-size': 'int', 'multi-auto-hide': 'boolean', 'multi-grid-size': 'int', 'time-format': 'int', 'rewind-time': 'int' }; const SETTINGS_RECOMMENDED = { 'animate_thumbnails': true, 'disable-big-previews': false, 'hide-vid-logo': true, 'rewind': true, 'smargins': true, 'no-socials': true, 'no-merch-links': true, 'no-surveys': true, 'auto-chat-rules': true, 'chat-hide-subject-notices': true, 'blur-mute': false, 'duration': true, 'inactive-load': true, 'inactive-quality': false, 'error-quality': false, 'open-new-tab': false, 'chat-hide-tips': false, 'chat-hide-notices': false, 'chat-hide-greys': false, 'rec-no-audio': false, 'rec-no-video': false, 'multi-auto-hide': true, 'multi-auto-hide-priv': false, 'multi-auto-remove': false, 'multi-show-subject': true, 'multi-unmute-hover': false, 'multi-align-right': false, 'multi-max-quality': 1080 }; const LOCALES_DEFAULT = { 'error': 'error', 'msg_settings_exported': 'Exported to file', 'msg_settings_invalid': 'Settings file is invalid.', 'msg_settings_imported': 'Settings imported', 'sched_chance': 'chance of being online on', 'sched_in': 'In', 'sched_hour': 'hour', 'sched_hours': 'hours', 'sched_tomorrow': 'Tomorrow', 'sched_chance_short': 'chance', 'err_vurl_no_room': 'ERROR: Not in a room.', 'err_vurl_unk': 'ERROR: Unknown error.', 'err_vurl': 'ERROR: No video URL.', 'get_vsurl': 'Get video source URL', 'copied_to_clipboard': 'copied to clipboard', 'disabled': 'disabled', 'yes': 'yes', 'no': 'no', 'free': 'free', 'minutes': 'minutes', 'month': 'month', 'months': 'months', 'cam': 'cams', 'cams': 'cams', 'country': 'country', 'countries': 'countries', 'less_than_minute': 'less than minute', 'err_vid': 'ERROR: Unable to load video.', 'err_ss': 'ERROR: Failed to capture screenshot!', 'err_no_rec_video': 'ERROR: There is no video to record!', 'start_rec': 'Start recording', 'stop_rec': 'Stop recording', 'err_rec_save': 'ERROR: Failed to save recording.', 'show_more_menu': 'Show more menu', 'multi_add': 'Add to multi cam viewer', 'passworded': 'passworded', 'multi_no_video_hls': 'error: no video available', 'multi_btn_exit_fscreen': 'EXIT FULLSCREEN', 'multi_btn_enter_fscreen_mini': 'ENTER SEMI FULLSCREEN (WITH ADDRESS BAR)', 'multi_btn_enter_fscreen': 'ENTER FULLSCREEN', 'r_bn': 'You are banned from this room. Chaturbate Enhancer bypassed this for you!', 'set_blocklist_info_current': 'You currently hide', 'btn_hide_room': 'Hide cam', 'btn_unhide_room': 'Unhide cam', 'fs': 'Fullscreen', 'fsx': 'Exit Fullscreen', 'vol': 'Volume', 'mute': 'Mute', 'unmute': 'Unmute', 'save_rewind_t': 'Save rewind video file', 'save_rewind_since_last_save': 'Saves only footage since the last save.', 'live': 'live', 'pause': 'Pause', 'play': 'Play', 'pt_index': 'Featured', 'pt_index_f': 'Women', 'pt_index_m': 'Men', 'pt_index_c': 'Couples', 'pt_index_t': 'Trans', 'pt_followed_cams': 'Followed Cams', 'pt_followed_cams_f': 'Followed Women Cams', 'pt_followed_cams_m': 'Followed Men Cams', 'pt_followed_cams_c': 'Followed Couple Cams', 'pt_followed_cams_t': 'Followed Trans Cams', 'pt_tags': 'Featured Tags', 'pt_tags_f': 'Women Tags', 'pt_tags_m': 'Men Tags', 'pt_tags_c': 'Couple Tags', 'pt_tags_t': 'Trans Tags', 'pt_discover': 'Discover Cams', 'pt_discover_f': 'Discover Women Cams', 'pt_discover_m': 'Discover Men Cams', 'pt_discover_c': 'Discover Couple Cams', 'pt_discover_t': 'Discover Trans Cams', 'pt_spy_on_cams': 'Private Cams', 'pt_spy_on_cams_f': 'Private Women Cams', 'pt_spy_on_cams_m': 'Private Men Cams', 'pt_spy_on_cams_c': 'Private Couple Cams', 'pt_spy_on_cams_t': 'Private Trans Cams', 'pt_random': 'Random Cam', 'pt_random_f': 'Random Woman', 'pt_random_m': 'Random Man', 'pt_random_c': 'Random Couple', 'pt_random_t': 'Random Trans', "cc_ad": "Andorra", "cc_ae": "United Arab Emirates", "cc_af": "Afghanistan", "cc_ag": "Antigua and Barbuda", "cc_ai": "Anguilla", "cc_al": "Albania", "cc_am": "Armenia", "cc_an": "Netherlands Antilles", "cc_ao": "Angola", "cc_aq": "Antarctica", "cc_ar": "Argentina", "cc_as": "American Samoa", "cc_at": "Austria", "cc_au": "Australia", "cc_aw": "Aruba", "cc_az": "Azerbaijan", "cc_ba": "Bosnia and Herzegovina", "cc_bb": "Barbados", "cc_bd": "Bangladesh", "cc_be": "Belgium", "cc_bf": "Burkina Faso", "cc_bg": "Bulgaria", "cc_bh": "Bahrain", "cc_bi": "Burundi", "cc_bj": "Benin", "cc_bm": "Bermuda", "cc_bn": "Brunei", "cc_bo": "Bolivia", "cc_br": "Brazil", "cc_bs": "Bahamas", "cc_bt": "Bhutan", "cc_bv": "Bouvet Island", "cc_bw": "Botswana", "cc_by": "Belarus", "cc_bz": "Belize", "cc_ca": "Canada", "cc_cf": "Central African Republic", "cc_ch": "Switzerland", "cc_ci": "Côte d'Ivoire", "cc_ck": "Cook Islands", "cc_cl": "Chile", "cc_cm": "Cameroon", "cc_cn": "China", "cc_co": "Colombia", "cc_cr": "Costa Rica", "cc_cu": "Cuba", "cc_cv": "Cape Verde", "cc_cx": "Christmas Island", "cc_cy": "Cyprus", "cc_cz": "Czech Republic", "cc_de": "Germany", "cc_dj": "Djibouti", "cc_dk": "Denmark", "cc_dm": "Dominica", "cc_do": "Dominican Republic", "cc_dz": "Algeria", "cc_ec": "Ecuador", "cc_ee": "Estonia", "cc_eg": "Egypt", "cc_eh": "Western Sahara", "cc_er": "Eritrea", "cc_es": "Spain", "cc_et": "Ethiopia", "cc_fi": "Finland", "cc_fj": "Fiji", "cc_fk": "Falkland Islands [Islas Malvinas]", "cc_fm": "Micronesia", "cc_fo": "Faroe Islands", "cc_fr": "France", "cc_ga": "Gabon", "cc_gb": "United Kingdom", "cc_gd": "Grenada", "cc_ge": "Georgia", "cc_gf": "French Guiana", "cc_gg": "Guernsey", "cc_gh": "Ghana", "cc_gi": "Gibraltar", "cc_gl": "Greenland", "cc_gm": "Gambia", "cc_gn": "Guinea", "cc_gp": "Guadeloupe", "cc_gq": "Equatorial Guinea", "cc_gr": "Greece", "cc_gs": "South Georgia and the South Sandwich Islands", "cc_gt": "Guatemala", "cc_gu": "Guam", "cc_gw": "Guinea-Bissau", "cc_gy": "Guyana", "cc_hk": "Hong Kong", "cc_hm": "Heard Island and McDonald Islands", "cc_hn": "Honduras", "cc_hr": "Croatia", "cc_ht": "Haiti", "cc_hu": "Hungary", "cc_id": "Indonesia", "cc_ie": "Ireland", "cc_il": "Israel", "cc_im": "Isle of Man", "cc_in": "India", "cc_io": "British Indian Ocean Territory", "cc_iq": "Iraq", "cc_ir": "Iran", "cc_is": "Iceland", "cc_it": "Italy", "cc_je": "Jersey", "cc_jm": "Jamaica", "cc_jo": "Jordan", "cc_jp": "Japan", "cc_ke": "Kenya", "cc_kg": "Kyrgyzstan", "cc_kh": "Cambodia", "cc_ki": "Kiribati", "cc_km": "Comoros", "cc_kn": "Saint Kitts and Nevis", "cc_kp": "North Korea", "cc_kr": "South Korea", "cc_kw": "Kuwait", "cc_ky": "Cayman Islands", "cc_kz": "Kazakhstan", "cc_la": "Laos", "cc_lb": "Lebanon", "cc_lc": "Saint Lucia", "cc_li": "Liechtenstein", "cc_lk": "Sri Lanka", "cc_lr": "Liberia", "cc_ls": "Lesotho", "cc_lt": "Lithuania", "cc_lu": "Luxembourg", "cc_lv": "Latvia", "cc_ly": "Libya", "cc_ma": "Morocco", "cc_mc": "Monaco", "cc_md": "Moldova", "cc_me": "Montenegro", "cc_mg": "Madagascar", "cc_mh": "Marshall Islands", "cc_ml": "Mali", "cc_mm": "Myanmar [Burma]", "cc_mn": "Mongolia", "cc_mo": "Macau", "cc_mp": "Northern Mariana Islands", "cc_mq": "Martinique", "cc_mr": "Mauritania", "cc_ms": "Montserrat", "cc_mt": "Malta", "cc_mu": "Mauritius", "cc_mv": "Maldives", "cc_mw": "Malawi", "cc_mx": "Mexico", "cc_my": "Malaysia", "cc_mz": "Mozambique", "cc_na": "Namibia", "cc_nc": "New Caledonia", "cc_ne": "Niger", "cc_nf": "Norfolk Island", "cc_ng": "Nigeria", "cc_ni": "Nicaragua", "cc_nl": "Netherlands", "cc_no": "Norway", "cc_np": "Nepal", "cc_nr": "Nauru", "cc_nu": "Niue", "cc_nz": "New Zealand", "cc_om": "Oman", "cc_pa": "Panama", "cc_pe": "Peru", "cc_pf": "French Polynesia", "cc_pg": "Papua New Guinea", "cc_ph": "Philippines", "cc_pk": "Pakistan", "cc_pl": "Poland", "cc_pm": "Saint Pierre and Miquelon", "cc_pn": "Pitcairn Islands", "cc_pr": "Puerto Rico", "cc_pt": "Portugal", "cc_pw": "Palau", "cc_py": "Paraguay", "cc_qa": "Qatar", "cc_re": "Réunion", "cc_ro": "Romania", "cc_rs": "Serbia", "cc_ru": "Russia", "cc_rw": "Rwanda", "cc_sa": "Saudi Arabia", "cc_sb": "Solomon Islands", "cc_sc": "Seychelles", "cc_sd": "Sudan", "cc_se": "Sweden", "cc_sg": "Singapore", "cc_sh": "Saint Helena", "cc_si": "Slovenia", "cc_sj": "Svalbard and Jan Mayen", "cc_sk": "Slovakia", "cc_sl": "Sierra Leone", "cc_sm": "San Marino", "cc_sn": "Senegal", "cc_so": "Somalia", "cc_sr": "Suriname", "cc_st": "São Tomé and Príncipe", "cc_sv": "El Salvador", "cc_sy": "Syria", "cc_sz": "Swaziland", "cc_tc": "Turks and Caicos Islands", "cc_td": "Chad", "cc_tf": "French Southern Territories", "cc_tg": "Togo", "cc_th": "Thailand", "cc_tj": "Tajikistan", "cc_tk": "Tokelau", "cc_tl": "Timor-Leste", "cc_tm": "Turkmenistan", "cc_tn": "Tunisia", "cc_to": "Tonga", "cc_tr": "Turkey", "cc_tt": "Trinidad and Tobago", "cc_tv": "Tuvalu", "cc_tw": "Taiwan", "cc_tz": "Tanzania", "cc_ua": "Ukraine", "cc_ug": "Uganda", "cc_us": "United States", "cc_uy": "Uruguay", "cc_uz": "Uzbekistan", "cc_va": "Vatican City", "cc_vc": "Saint Vincent and the Grenadines", "cc_ve": "Venezuela", "cc_vg": "British Virgin Islands", "cc_vi": "U.S. Virgin Islands", "cc_vn": "Vietnam", "cc_vu": "Vanuatu", "cc_wf": "Wallis and Futuna", "cc_ws": "Samoa", "cc_xk": "Kosovo", "cc_ye": "Yemen", "cc_yt": "Mayotte", "cc_za": "South Africa", "cc_zm": "Zambia", "cc_zw": "Zimbabwe" }; const VALID_COUNTRIES = [ 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', 'bm', 'bn', 'bo', 'bq', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cu', 'cv', 'cw', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'ss', 'st', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'za', 'zm', 'zw' ]; const multiLinkHTML = `<a href="/multicam/" class="HeaderNavBar__link"><div class="type--smpx type--medium textColor HeaderNavBar__link-text">multi cam viewer</div></a><span class="HeaderNavBar__link" style="cursor: pointer;" id="cb-enh-toggle-settings"><div class="type--smpx type--medium textColor HeaderNavBar__link-text">CHATURBATE ENHANCER <span class="ce-loc" data-ce-loc="settings">Settings</span></div></span><a href="/random/" class="HeaderNavBar__link" id="cb-enh-random-cam"><div class="type--smpx type--medium textColor HeaderNavBar__link-text ce-loc" data-ce-loc="random_cam">RANDOM CAM</div></a>`; const gBlocklistCountryStyles = []; const gBlocklistCountries = []; let gSettings = {}; let gLocales = {}; let gIntvCurrentHover = null; let gLastLoadedThumbReqTime = 0; let gCurrentBroadcaster = null; let gCurrentBroadcasterGender = null; let gCurrentRoomIsInaccessible = false; let gVideoControlsModalShown = false; let gVideoControlsCurrentVideoHolder = null; let gCapturingScreenshot = false; let gPageTitle = null; let gRecording = false; let gRecordingSession = null; let gRecordingExitWarn = false; let gIntvRecordingUpdate = null; let gRecordAudio = true; let gRecordVideo = true; let gIsInMultiView = false; let gUnmuteCurrentVideoOnFocus = false; let gOpenRoomsInNewTab = false; let gWasMutedBySetting = false; function isFirefox() { return navigator.userAgent.toLowerCase().indexOf('firefox') !== -1; } function isMac() { return navigator.userAgentData ? navigator.userAgentData.platform === 'macOS' : navigator.platform.toLowerCase().includes('mac'); } function isGreasemonkey() { if(typeof GM === 'undefined' || typeof GM.info === 'undefined') { return null; } if(!GM.info) { return null; } if(!GM.info.scriptHandler) { return null; } if(typeof GM.info.scriptHandler !== 'string') { return null; } return GM.info.scriptHandler.toLowerCase() == 'greasemonkey'; } const gDoNeedAlternativeWindowApproach = isFirefox() && isGreasemonkey(); class HLSLoader extends Hls.DefaultConfig.loader { constructor(config) { super(config); this.stats ??= {}; } load(context, config, callbacks) { this.req = GM.xmlHttpRequest({ method: 'GET', url: context.url, responseType: context.responseType, onload: (res) => { this.stats.tload = performance.now(); if(res.status >= 200 && res.status < 300) { callbacks.onSuccess( { data: res.response }, this.stats, context, null ); } else { callbacks.onError( { code: res.status, text: res.statusText }, context, null ); } }, onerror: () => { callbacks.onError( { code: 0, text: 'error' }, context, null ); } }); } abort() { this.req?.abort?.(); this.req = null; } destroy() { this.abort(); } } function createHLS() { if(gDoNeedAlternativeWindowApproach) { return new Hls({ autoStartLoad: false, loader: HLSLoader, enableWorker: false }); } return new Hls({ autoStartLoad: false, }); } function runFunction(callback, args = []) { const funcName = '__cnh__fnc__' + (callback.name || 'ffnc'); const textContent = `const ${funcName} = ${callback.toString()};${funcName}(${args.map(a => JSON.stringify(a)).join(',')});`; addElementFallback(document.documentElement, 'script', { textContent: textContent }); } function tryRunFunction(callback, args = []) { if(!gDoNeedAlternativeWindowApproach) { callback(...args); return; } if(document.documentElement) { runFunction(callback, args); return; } gFunctionsToRun.push([callback, args]); } function addFunction(callback) { const funcName = callback.name; const textContent = `window.__cnh__fnc__.${funcName} = ${callback.toString()};`; addElementFallback(document.documentElement, 'script', { textContent: textContent }); } function tryAddFunction(callback) { if(!gDoNeedAlternativeWindowApproach) { unsafeWindow.__cnh__fnc__[callback.name] = callback; return; } if(document.documentElement) { addFunction(callback); return; } gFunctionsToAdd.push(callback); } function localize() { if(!gLocales) { return; } document.querySelectorAll('.ce-loc').forEach(function(el) { let v = el.dataset.ceLoc; if(gLocales[v]) { if(el.nodeName === 'INPUT') { el.value = gLocales[v]; } else { el.textContent = gLocales[v]; } } }); } function loadLocales(lang) { xmlhttpRequest({ method: 'GET', url: 'https://cb-enh.improper.dev/locale/' + lang + '.json?key=' + acre6, timeout: 60*1*1000, onload: function(resp) { let data; try { data = JSON.parse(resp.responseText); } catch(e) { return; } gLocales = data['locales']; localize(); } }); } function localizeNextTick() { runNextTick(localize); } function getLocale(name) { if(gLocales && gLocales[name]) { return gLocales[name]; } if(LOCALES_DEFAULT[name]) { return LOCALES_DEFAULT[name]; } return ''; } function getSiteLang() { return document.documentElement?.getAttribute('lang'); } function getUserLang() { return navigator.language || navigator.userLanguage; } function getUserOrSiteLang() { return getUserLang() || getSiteLang(); } function isLangUS(lang) { if(!lang) { return false; } lang = String(lang).toLowerCase().replace('_', '-'); return lang === 'en' || lang.startsWith('en-'); } function maybeLoadLocales() { if(gIsFullVideoPage) { return; } const lang = getSiteLang(); if(lang !== 'en') { loadLocales(lang); } } function getSetting(name) { if(typeof gSettings[name] !== 'undefined') { return gSettings[name]; } return null; } function settingsSave() { localStorage.setItem('cb-enh-settings', JSON.stringify(gSettings)); } function setSetting(name, value) { gSettings[name] = value; settingsSave(); } function loadSettings() { let settings = localStorage.getItem('cb-enh-settings'); if(settings !== null) { try { settings = JSON.parse(settings); } catch(e) { settings = null; } } if(!settings || typeof settings !== 'object' || Array.isArray(settings)) { settings = {}; } settingsValidateAndLoad(settings); blocklistCountryLoad(); } loadSettings(); const gBroadcastChannel = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('__cnh__broadcast') : null; if(gBroadcastChannel) { gBroadcastChannel.onmessage = (e) => { if(e.data.type === 'mute') { document.querySelectorAll('video').forEach(elVideo => { elVideo.muted = true; }); return; } } } function settingsExport() { const dataMulti = multiLoadRoomsSettings(); let data = { 'version': gVersion, 'settings': gSettings, 'multi': dataMulti.rooms, 'multiGridColumns': dataMulti.gridColumns, 'multiSizes': dataMulti.sizes, 'multiLayoutMode': dataMulti.layoutMode, 'blocklist': gBlocklistRooms }; data = settingsValidateAll(data); if(!data) { return; } const json = JSON.stringify(data); const blob = new Blob([json], { type: 'application/json' }); const objectURL = URL.createObjectURL(blob); const date = new Date(); const fname = getFileName('cb_enhancer_settings', '.json', date); download({ url: objectURL, name: fname, onloadend: () => { const elInfo = document.getElementById('cb-enh-settings-export-info'); clearElement(elInfo); const elSpan1 = createLocalizedSpan('msg_settings_exported'); elInfo.append(elSpan1); elInfo.append(document.createTextNode(': ')); const elSpan2 = document.createElement('span'); elSpan2.innerText = fname; elSpan2.className = 'cb-enh-click-select'; document.getElementById('cb-enh-settings-export-info').append(elSpan2); } }); } function settingsImportError() { document.getElementById('cb-enh-settings-export-info').innerText = getLocale('error').toUpperCase() + ': ' + getLocale('msg_settings_invalid'); } function settingsValidateAll(data) { if(typeof data.version !== 'string') { return false; } if(!data.settings || typeof data.settings !== 'object' || Array.isArray(data.settings)) { return false; } if(!Array.isArray(data.multi)) { return false; } if(!Array.isArray(data.blocklist)) { return false; } for(const v of data.multi) { if(typeof v !== 'string') { return false; } if(!v.match(/^[a-zA-Z0-9_]+$/)) { return false; } } for(const v of data.blocklist) { if(typeof v !== 'string') { return false; } if(!v.match(/^[a-zA-Z0-9_]+$/)) { return false; } } const settingsValidatedSettings = settingsValidateSettings(data.settings); if(!settingsValidatedSettings) { return false; } const multiGridColumns = Number.isSafeInteger(data.multiGridColumns) && data.multiGridColumns > 0 ? data.multiGridColumns : gMultiGridColumnCount; const settingsValidated = { 'version': data.version, 'settings': settingsValidatedSettings, 'multi': data.multi, 'multiGridColumns': multiGridColumns, 'multiSizes': multiValidateCamSizes(data.multiSizes, data.multiGridColumns), 'multiLayoutMode': data.multiLayoutMode === 'custom' ? 'custom' : 'preset', 'blocklist': data.blocklist }; return settingsValidated; } function settingsValidateSettings(dataSettings) { const hiddenGenders = []; const allowedGenders = ['f', 'c', 'm', 't']; if(Array.isArray(dataSettings['featured-hidden-genders'])) { for(const v of dataSettings['featured-hidden-genders']) { if(!allowedGenders.includes(v)) { continue; } hiddenGenders.push(v); } } const randomGenders = []; if(Array.isArray(dataSettings['multi-random-genders'])) { for(const v of dataSettings['multi-random-genders']) { if(!allowedGenders.includes(v)) { continue; } randomGenders.push(v); } } const blocklistCountries = []; if(Array.isArray(dataSettings['blocklist-cc'])) { for(const v of dataSettings['blocklist-cc']) { if(!VALID_COUNTRIES.includes(v)) { continue; } blocklistCountries.push(v); } } const settingsValidatedSettings = { 'featured-hidden-genders': hiddenGenders, 'multi-random-genders': randomGenders, 'blocklist-cc': blocklistCountries }; for(const [k, v] of Object.entries(dataSettings)) { const requiredType = SETTINGS_SCHEMA[k]; if(!requiredType) { continue; } if(requiredType === 'boolean') { if(typeof v !== requiredType) { continue; } } else if(requiredType === 'int') { if(typeof v !== 'number' || !Number.isInteger(v)) { continue; } } else { continue; } settingsValidatedSettings[k] = v; } return settingsValidatedSettings; } function settingsValidateAllAndLoad(data) { const settingsValidatedAll = settingsValidateAll(data); if(!settingsValidatedAll) { return false; } gSettings = settingsValidatedAll.settings; settingsSave(); blocklistCountryLoad(); if(isMultiViewPage()) { gMultiCustomGridActive = false; multiRemoveAllRooms(); } localStorage.setItem('cb-enh-multicam', JSON.stringify({ 'rooms': settingsValidatedAll.multi, 'gridColumns': settingsValidatedAll.multiGridColumns, 'sizes': settingsValidatedAll.multiSizes, 'layoutMode': settingsValidatedAll.multiLayoutMode })); if(isMultiViewPage()) { gMultiCustomGridActive = parseInt(getSetting('multi-grid-size')) === gMultiCustomGridSize && settingsValidatedAll.multiLayoutMode !== 'custom'; multiUpdateSettings(); multiUpdateRooms(); multiUpdateAllVideosQuality(); } blocklistClear(); for(const v of settingsValidatedAll.blocklist) { blocklistAdd(v); } blocklistSave(); queryAndRun('.cb-enh-multi-max-quality', el => { el.value = multiGetMaxHeight(); }); updateSettingsWindow(); return true; } function settingsValidateAndLoad(data) { const settingsValidatedSettings = settingsValidateSettings(data); if(!settingsValidatedSettings) { return; } gSettings = { ...gSettings, ...settingsValidatedSettings }; return true; } function settingsImportOnFileDrop(e) { const file = e.target.files?.[0]; if(!file) { return; } const ext = getFileExtension(file.name || ''); if(ext !== 'json') { settingsImportError(); return; } file.text().then((text) => { let data; try { data = JSON.parse(text); } catch { settingsImportError(); return; } if(!settingsValidateAllAndLoad(data)) { settingsImportError(); return; } applySettings(); document.getElementById('cb-enh-settings-export-info').innerText = getLocale('msg_settings_imported') + '!'; }); } function shouldDoFakeVisibility() { if(gRecording) { return true; } if(isThereCurrentVideo()) { return getSetting('inactive-quality') === false; } if(getSetting('inactive-load') || getSetting('inactive-load') === null) { return true; } return false; } function updateFakeVisibility() { unsafeWindow.__cnh__2.gFakeVisibility = shouldDoFakeVisibility(); } const value_gFakeVisibility = shouldDoFakeVisibility(); function initWindowObjects(fakeVisibility) { let windowObject = window; if(typeof unsafeWindow !== 'undefined') { windowObject = unsafeWindow; } if(typeof windowObject.__cnh__2 === 'undefined') { windowObject.__cnh__2 = { gFakeVisibility: fakeVisibility, acrtt9: false }; } if(typeof windowObject.__cnh__fnc__ === 'undefined') { windowObject.__cnh__fnc__ = {}; } } tryRunFunction(initWindowObjects, [value_gFakeVisibility]); const REGION_LINKS = { 0: '/asian-cams/', 1: '/euro-russian-cams/', 2: '/north-american-cams/', 3: '/south-american-cams/', 4: '/other-region-cams/' }; const SVG_CLOSE_ICON = '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M256 874.088 181.912 800l224-224-224-224L256 277.912l224 224 224-224L778.088 352l-224 224 224 224L704 874.088l-224-224-224 224Z"/></svg>'; const SVG_MORE_ICON = '<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M479.788-192Q450-192 429-213.212q-21-21.213-21-51Q408-294 429.212-315q21.213-21 51-21Q510-336 531-314.788q21 21.213 21 51Q552-234 530.788-213q-21.213 21-51 21Zm0-216Q450-408 429-429.212q-21-21.213-21-51Q408-510 429.212-531q21.213-21 51-21Q510-552 531-530.788q21 21.213 21 51Q552-450 530.788-429q-21.213 21-51 21Zm0-216Q450-624 429-645.212q-21-21.213-21-51Q408-726 429.212-747q21.213-21 51-21Q510-768 531-746.788q21 21.213 21 51Q552-666 530.788-645q-21.213 21-51 21Z"/></svg>'; const SVG_PIP_ICON = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="white"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Zm280-200h320v-240H440v240Zm80-80v-80h160v80H520Z"/></svg>'; const SVG_CAMERA_ICON = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-260q75 0 127.5-52.5T660-440q0-75-52.5-127.5T480-620q-75 0-127.5 52.5T300-440q0 75 52.5 127.5T480-260Zm0-80q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM160-120q-33 0-56.5-23.5T80-200v-480q0-33 23.5-56.5T160-760h126l74-80h240l74 80h126q33 0 56.5 23.5T880-680v480q0 33-23.5 56.5T800-120H160Zm0-80h640v-480H638l-73-80H395l-73 80H160v480Zm320-240Z"/></svg>'; const SVG_VIDEO_CONTROLS = '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 -960 960 960" fill="#e3e3e3"><path d="M480-160H160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v200h-80v-200H160v480h320v80ZM380-300v-360l280 180-280 180ZM714-40l-12-60q-12-5-22.5-10.5T658-124l-58 18-40-68 46-40q-2-14-2-26t2-26l-46-40 40-68 58 18q11-8 21.5-13.5T702-380l12-60h80l12 60q12 5 22.5 11t21.5 15l58-20 40 70-46 40q2 12 2 25t-2 25l46 40-40 68-58-18q-11 8-21.5 13.5T806-100l-12 60h-80Zm40-120q33 0 56.5-23.5T834-240q0-33-23.5-56.5T754-320q-33 0-56.5 23.5T674-240q0 33 23.5 56.5T754-160Z"/></svg>'; function patch_Object_defineProperty() { let windowObject = window; if(typeof unsafeWindow !== 'undefined') { windowObject = unsafeWindow; } const _defineProperty = Object.defineProperty; windowObject.Object.defineProperty = function(obj, prop, descriptor) { if(prop === 'hide_entrance_terms') { retur; } return _defineProperty.call(Object, obj, prop, descriptor); }; } tryRunFunction(patch_Object_defineProperty); function getCookie(name) { const nameEQ = encodeURIComponent(name) + "="; const ca = document.cookie.split(';'); for(let i = 0; i < ca.length; i++) { let c = ca[i]; while(c.charAt(0) === ' ') { c = c.substring(1, c.length); } if(c.indexOf(nameEQ) === 0) { return decodeURIComponent(c.substring(nameEQ.length, c.length)); } } return null; } function doUserPrefsDarkMode() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } function isDarkModeSet() { return document.body && document.body.classList.contains('darkmode'); } function getCurrentBaseDomain() { const parts = window.location.hostname.split('.'); if(parts.length <= 2) { return window.location.hostname; } return parts.slice(-2).join('.'); } const gBaseDomain = getCurrentBaseDomain(); function enableDarkMode() { if(!doUserPrefsDarkMode()) { return; } makeSureRuns(() => { if(document.body) { document.body.classList.add('darkmode'); } }); if(getCookie('theme_name') === null) { document.cookie = 'theme_name=darkmode; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/'; document.cookie = 'theme_name=darkmode; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.' + gBaseDomain; } } function isMissingDarkModePage() { const missingDarkModePages = [ '/my_collection/', '/fanclub/join/', '/supporter/upgrade/', '/terms/', '/privacy/', '/2257/', '/law_enforcement/', '/billingsupport/', '/auth/login/', '/accounts/register/' ]; let res = false; missingDarkModePages.forEach((v) => { if(window.location.pathname.startsWith(v)) { res = true; return false; } return true; }); return res; } if(doUserPrefsDarkMode()) { enableDarkMode(); const bodyObserver = new MutationObserver(() => { if(document.body) { bodyObserver.disconnect(); document.body.classList.add('darkmode'); if(isMissingDarkModePage()) { document.body.classList.add('darkmode-cenhf'); } } }); bodyObserver.observe(document.documentElement || document, { childList: true, subtree: true }); } if(getCookie('agreeterms') !== '1') { document.cookie = 'agreeterms=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/'; document.cookie = 'agreeterms=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.' + gBaseDomain; window.location.reload(); } function isMultiViewPage() { return window.location.pathname === '/multicam/'; } function isRandomPage() { return window.location.pathname.startsWith('/random/'); } function isRoomPage() { return document?.querySelector('.top-section')?.classList?.contains('roomPage'); } function isAG() { return getCookie('AG_Key') !== null; } function define(obj, prop, value) { Object.defineProperty(obj, prop, { get() { return value; } }); } function patchXHR(xhr, statusCode, responseText) { define(xhr, 'status', statusCode); define(xhr, 'statusText', statusCode === 200 ? 'OK' : 'Error'); define(xhr, 'responseText', responseText); define(xhr, 'response', responseText); } function createRoomData(data, username) { return JSON.stringify({ room_pass: '', last_pass: '', chat_rules: '', is_supporter: false, needs_supporter_to_pm: false, apps_running: '', allow_private_shows: false, private_show_price: 0, private_min_minutes: 0, allow_show_recordings: false, allow_anonymous_tipping: false, spy_private_show_price: 0, private_show_id: '', low_satisfaction_score: false, is_age_verified: false, hidden_message: '', following: false, follow_notification_frequency: '', is_moderator: false, token_balance: 0, has_studio: false, is_mobile: false, ignored_emoticons: [], hide_satisfaction_score: false, tips_in_past_24_hours: 0, last_vote_in_past_90_days_down: false, dismissible_messages: [], show_mobile_site_banner_link: false, fan_club_is_member: false, performer_has_fanclub: false, fan_club_paid_with_tokens: false, satisfaction_score: { up_votes: 0, down_votes: 0, percent: 0 }, tfa_enabled: false, chat_settings: { font_color: '', font_family: '', font_size: '', show_emoticons: true, emoticon_autocomplete_delay: 0, highest_token_color: '', sort_users_key: 'alphabetical', mod_expire: 0, room_entry_for: 'all', room_leave_for: 'all', c2c_notify_limit: 0, silence_broadcasters: '', ignored_users: '', allowed_chat: '', tip_volume: 50, collapse_notices: false }, asp_auth_url: '', asp_app_directory_v3_enabled: false, browser_id: '', userlist_color: '', active_password: false, is_tip_menu_enabled: false, broadcaster_username: data.broadcaster_username || username, room_uid: data.room_uid || '', room_title: data.room_title || '', room_status: data.room_status || 'offline', viewer_username: data.viewer_username || '', broadcaster_gender: data.broadcaster_gender || '', hls_source: data.hls_source || '', is_widescreen: !!data.is_widescreen, is_portrait: !!data.is_portrait, edge_region: data.edge_region || '', exploring_hashtag: data.exploring_hashtag || '', source_name: data.source_name || '', age: data.age || 0, num_viewers: data.num_viewers || 0, cmaf_edge: !!data.cmaf_edge }); } function clearIntervalEx(intv) { if(typeof intv !== 'undefined' && intv) { clearInterval(intv); } return null; } function clearTimeoutEx(intv) { if(typeof intv !== 'undefined' && intv) { clearTimeout(intv); } return null; } function setIntervalRunFirst(callback, timeout) { if(callback() === false) { return null; } return setInterval(callback, timeout); } function getRandomStr(len) { let res = ''; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-'; const charsLen = chars.length; let i = 0; while(i < len) { res += chars.charAt(Math.floor(Math.random() * charsLen)); i += 1; } return res; } const gtrrrit = {}; function gtrrft(c = null) { let s = 'cnhdr' + getRandomStr(50); if(!c) { return s; } const i = 5 + (Math.random() * (s.length - 5 + 1) | 0); s = s.slice(0, i) + c + s.slice(i); if(!(c in gtrrrit)) { gtrrrit[c] = s; } return gtrrrit[c]; } const gIsAG = !gIsFullVideoPage && isAG(); const gAChat = isMultiViewPage() || isRandomPage(); if(gIsAG) { let intvWaitForDataInit1; intvWaitForDataInit1 = setIntervalRunFirst(() => { if(typeof unsafeWindow.$reactAppContext === 'undefined') { return; } clearIntervalEx(intvWaitForDataInit1); Object.defineProperty(unsafeWindow.$reactAppContext, 'user_age_gated', { get() { return false; }, set(v) { return true; } }); return false; }, 10); let intvWaitForDataInit2; intvWaitForDataInit2 = setIntervalRunFirst(() => { if(typeof unsafeWindow.tsInstance === 'undefined') { return; } clearIntervalEx(intvWaitForDataInit2); Object.defineProperty(unsafeWindow.tsInstance.context, 'isAgeGated', { get() { return false; }, set(v) { return true; } }); return false; }, 10); if(typeof URLSearchParams !== 'undefined' && window?.location?.search) { const params = new URLSearchParams(window?.location?.search); if(params) { const next = params.get('next'); if(next && /^\/(?!\/)/.test(next)) { history.replaceState({}, '', next); window.dispatchEvent(new PopStateEvent('popstate', { state: {} })); } } } addStyle(`.msg-list-wrapper-split {line-height: 16pt !important;}`); } let gHbx = null; const gurlsx = [ '/api/chatvideocontext/', '/api/biocontext/', '/photo_videos/api/pvcontext/', '/api/more_like/' ]; const gurls = [ '/api/ts/chat/room-access/', '/api/ts/games/current/room/', '/api/ts/chatmessages/', '/api/public/asp/broadcast/applist/', '/push_service/', '/contest/log/' ]; const _xhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { if(gAChat && typeof url === 'string' && url.includes('/push_service/auth/')) { this._hook_block = true; } else if(gIsAG && typeof url === 'string' && url.includes('/api/chatvideocontext/')) { this._hook_ag = true; const m = url.match(/chatvideocontext\/([^/]+)/); this._username = m[1]; } if(gHbx !== null && typeof url === 'string') { for(const v of gurlsx) { if(url.includes(v)) { this._h_hbx = true; this._h_hbx_a = v; } } if(!this._h_hbx) { for(const v of gurls) { if(url.includes(v)) { return; } } } } return _xhrOpen.apply(this, arguments); }; const _xhrSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function() { if(this._hook_block) { return; } if(this._hook_ag) { const xhr = this; const req = new XMLHttpRequest(); req.addEventListener('load', function() { let data; try { data = JSON.parse(this.responseText); } catch(e) { return; } patchXHR(xhr, 200, createRoomData(data, xhr._username)); xhr.dispatchEvent(new Event('load')); xhr.dispatchEvent(new Event('loadend')); }); req.timeout = 60*1*1000; req.open('GET', '/api/livecampreviewcontext/' + xhr._username + '/'); req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); req.withCredentials = true; req.send(); return; } if(this._h_hbx) { const xhr = this; const url = 'https://' + gBaseDomain + xhr._h_hbx_a + gHbx + '/'; getJSON_noNative(url, data => { patchXHR(xhr, 200, JSON.stringify(data)); xhr.dispatchEvent(new Event('load')); xhr.dispatchEvent(new Event('loadend')); }, true); return; } return _xhrSend.apply(this, arguments); }; if(isMultiViewPage()) { addStyle(`.content, #theatermode-root {display: none !important;}`); } const gMultiGridColumnCount = 60; const gMultiMinColumnSpan = Math.ceil(gMultiGridColumnCount / 10); const gMultiCustomGridSize = 1; if(!gIsFullVideoPage) { addStyle(`.photoVideoDetailSection img {filter: unset !important;}.userUpload div {background: none !important;}.psContainer .lockOverlayBg, .smContainer .lockOverlayBg {display: none !important;}a[data-testid="photo-video-item"] > img[data-testid="lock-icon"] {display: none !important;}a[data-testid="photo-video-item"] > div:not(.link) {background: none !important;}.ad, .vote-banner {display: none !important;}.cb-enh-avatar {margin-left: 10px;border: 1px solid #bfbfbf;width: 150px;height: 150px;background-color: #ebebeb;margin-bottom: 5px;background-size: 100% 100%;position: relative;display: none;}.darkmode .cb-enh-avatar {border-color: #2d3e50;background-color: #202c39;}.cb-enh-avatar, .cb-enh-avatar img {border-radius: 150px;}.cb-enh-avatar img {width: 100%;height: 100%;opacity: 0;position: absolute;left: 0;top: 0;-webkit-user-drag: none;-webkit-app-region: no-drag;user-drag: none;app-region: no-drag;pointer-events: none;-webkit-touch-callout: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}.cb-enh-offline-snapshot {display: block;border-radius: 4px;margin-top: 10px;}.cb-enh-footer {font-size: 14px;color: #341b00;font-weight: bold;}.darkmode .cb-enh-footer {color: #efefef;}.cb-enh-footer a {color: inherit !important;text-decoration: underline;}tr:not(.smContainer) .contentText .previewBorder, div[data-testid="photoVideoPreview"] {width: 190px !important;height: 135px !important;}div[data-testid="photoVideoPreview"] .previewBorder {width: 100%;height: 100%;}a[data-testid="photo-video-item"] .tokenText {top: unset !important;bottom: 11px !important;right: -5px !important;}a[data-testid="photo-video-item"] img[src$="/tsdefaultassets/video.svg"] {top: 4px !important;right: 4px !important;}a[data-testid="photo-video-item"] img[src$="/tsdefaultassets/no-audio.svg"] {top: 4px !important;right: 26px !important;}body.cb-enh-nsc div[data-testid="bio-tab-about-me-value"] a[href^="/external_link/?url="],body.cb-enh-nsc div[data-testid="bio-tab-wish-list-value"] a[href^="/external_link/?url="] {display: none !important;}body.cb-enh-nsc div[data-testid="bio-tab-about-me-value"] a:not([href^="/external_link/"])[href*="/socials/social_media/"],body.cb-enh-nsc div[data-testid="bio-tab-wish-list-value"] a:not([href^="/external_link/"])[href*="/socials/social_media/"] {display: none !important;}body.cb-enh-nsc .BioContents tr.smContainer:has(td[data-testid="bio-tab-social-media-value"]) {display: none !important;}body.cb-enh-nsc #social-media-icons {display: none !important;}body.cb-enh-nml a#merch, body.cb-enh-nml li:has(a#cbswag) {display: none !important;}tr:not(.smContainer):not(.psContainer) .contentText img,tr:not(.smContainer):not(.psContainer) .contentText li,tr:not(.smContainer):not(.psContainer) .contentText:not(.cb-enh-row-value) a,tr:not(.smContainer):not(.psContainer) .contentText p,tr:not(.smContainer):not(.psContainer) .contentText span {position: relative !important;top: 0 !important;left: 0 !important;}tr:not(.smContainer):not(.psContainer) .contentText *[style*="position: absolute;"] {position: relative !important;top: 0 !important;left: 0 !important;z-index: -10 !important;}div[data-testid="bio-tab-about-me-value"] a[href*="/accounts/register/"],div[data-testid="bio-tab-about-me-value"] a:not([href^="/external_link/"])[href*="campaign="],div[data-testid="bio-tab-about-me-value"] a:not([href^="/external_link/"])[href*="/in/"],div[data-testid="bio-tab-about-me-value"] a[href^="/external_link/?url="][href*="%2FStatus.aspx%3F"],div[data-testid="bio-tab-about-me-value"] a[href^="/external_link/?url="][href*="cbhours.com%2F"],div[data-testid="bio-tab-about-me-value"] a[href^="/external_link/?url="][href*="cbexplorer.com%2F"],div[data-testid="bio-tab-wish-list-value"] a[href*="/accounts/register/"],div[data-testid="bio-tab-wish-list-value"] a:not([href^="/external_link/"])[href*="campaign="],div[data-testid="bio-tab-wish-list-value"] a:not([href^="/external_link/"])[href*="/in/"],div[data-testid="bio-tab-wish-list-value"] a[href^="/external_link/?url="][href*="%2FStatus.aspx%3F"],div[data-testid="bio-tab-wish-list-value"] a[href^="/external_link/?url="][href*="cbhours.com%2F"],div[data-testid="bio-tab-wish-list-value"] a[href^="/external_link/?url="][href*="cbexplorer.com%2F"] {display: none !important;}tr:not(.smContainer):not(.psContainer) .contentText *:not(.fi):not(.cb-enh-schedule-hour):not(.cb-enh-schedule-tooltip-inner) {background: unset !important;}tr:not(.smContainer):not(.psContainer) .contentText * {cursor: auto !important;}tr:not(.smContainer):not(.psContainer) .contentText a {cursor: pointer !important;}tr:not(.smContainer):not(.psContainer) .contentText a * {cursor: pointer !important;}div[data-testid="bio-tab-about-me-value"] *,div[data-testid="bio-tab-wish-list-value"] * {overflow: hidden !important;}.cb-enh-video {max-width: 900px;margin: 0px;padding: 0px;width: 100%;height: 100%;object-fit: contain;background-color: rgba(0, 0, 0, 0);display: inline;border: 0;outline: 0;border-radius: 4px;}.cb-enh-video::-webkit-media-controls-play-button {display: none;}.cb-enh-video::-webkit-media-controls-timeline {display: none;}.cb-enh-video::-webkit-media-controls-current-time-display {display: none;}.cb-enh-video::-webkit-media-controls-timeline-container {display: none;}.cb-enh-video::-webkit-media-controls-time-remaining-display {display: none;}.cb-enh-chat-frame {width: 100%;max-width: 1400px;height: 700px;border: 0;border-radius: 4px;}#cb-enh-inac-load-chat {cursor: pointer;}.noselect {-webkit-touch-callout: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}.cb-enh-video-bar-btn {--bg: #880471;height: 15px;width: auto;position: relative;overflow: hidden;-webkit-tap-highlight-color: transparent;font-family: UbuntuMedium, Helvetica, Arial, sans-serif;font-size: 12px;padding: 3px 8px 2px;top: -4px;right: 1px;float: right;border-radius: 3px;cursor: pointer;margin-right: 5px;background-color: var(--bg);color: white;text-transform: uppercase;border-bottom-width: 0.8px;border-left-width: 0.8px;border-right-width: 0.8px;border-top-width: 0.8px;border: 1px solid var(--bg);}#cb-enh-video-controls-record {--bg: #090;overflow: visible;}#cb-enh-video-controls-record.cb-enh-active {--bg: #ff0000;}#cb-enh-recording-tooltip {top: calc(100% + 1px + 10px + 6px);left: 50%;transform: translateX(-50%);width: max-content;text-align: center;text-transform: none;pointer-events: none;color: #000;}.darkmode #cb-enh-recording-tooltip {color: white;}#cb-enh-video-controls-record.cb-enh-active > #cb-enh-recording-tooltip {display: block;}#cb-enh-recording-tooltip > .cb-enh-arrow {top: -11px;left: calc(50% - 10px);}.cb-enh-video-bar-btn-hide {--bg: #4f4f4f;}.cb-enh-video-bar-btn-hide.cb-enh-active {--bg: #890000;}.tabBar .fanclubButton, .tabBar .followButton, .tabBar .unfollowButton {line-height: 1.3 !important;}.cb-enh-video-bar-btn {line-height: 1.3 !important;}.cb-enh-tab-bar-modal {width: 500px;border-width: 1px;position: absolute;border-style: solid;border-radius: 4px;font-size: 14px;padding: 8px 8px 8px 8px;display: none;z-index: 15;line-height: 22px;box-shadow: rgba(0, 0, 0, 0.08) 0px 4px 16px;background-color: #fdfdfd;border: 1px solid #acacac;}.darkmode .cb-enh-tab-bar-modal {background-color: #19222c;border-color: #003061;color: #f0f0f0;}.cb-enh-arrow {position: absolute;width: 0;height: 0;border-left: 10px solid transparent;border-right: 10px solid transparent;}.cb-enh-arrow-down {border-top: 10px solid #0554a3;}.cb-enh-arrow-up {border-bottom: 10px solid #0554a3;}.videoPlayerDiv:not([style*='height: 100%; width: 100%;']) video.cb-enh-video-transformed {width: calc(100% - 10px) !important;}.vjs-fullscreen video.cb-enh-video-transformed {width: calc(100% - 10px) !important;}.videoPlayerDiv.cb-enh-video-rotated > canvas:not(.cb-enh-rewind-video-canvas) {display: none !important;}.videoPlayerDiv.cb-enh-video-rotated {background: none !important;}.cb-enh-video-controls-modal-btns {margin-top: 5px;}.cb-enh-vid-control-slider {width: 50%;}.entrance-terms--shown {position: inherit !important;top: inherit !important;left: inherit !important;right: inherit !important;bottom: inherit !important;overflow: inherit !important;background-color: #fff;visibility: inherit !important;}#entrance_terms_overlay {display: none !important;}#`+gtrrft('an')+` {width: 500px;height: 68px;box-sizing: border-box;font-size: 15px;overflow: hidden;display: inline-block;vertical-align: top;margin: 0px;float: right;color: #292929;}.darkmode #`+gtrrft('an')+` {color: #f2f2f2;}.`+gtrrft('ao')+` {position: relative;height: 100%;}.`+gtrrft('ann')+` {margin: 0;position: absolute;top: 50%;transform: translateY(-50%);right: 0%;margin-right: 4px;line-height: 15px;cursor: pointer;font-weight: bold;color: white;border: 1px solid #496b91;border-radius: 4px;background-color: #003168;max-width: 750px;cursor: pointer;transition: 0.2s background-color;padding: 4px;}.darkmode .`+gtrrft('ann')+` {background-color: #012247;}.`+gtrrft('ann')+`:hover {background-color: #003979;}.`+gtrrft('anst')+` {font-size: 12px;line-height: 11px;font-weight: normal;}#`+gtrrft('mspr')+` .`+gtrrft('anst')+` {margin-top: 4px;font-size: 15px;line-height: 17px;font-weight: normal;}.blurred-login-overlay > div > span, .blurred-login-overlay > div > hr {display: none;}.thumbnail_label {pointer-events: none;text-transform: uppercase;}.RoomCardThumbnail__labelContainer {pointer-events: none;}div[data-testid="quality-option"] {-webkit-touch-callout: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}div.slider[aria-label="Volume Slider"] div {-webkit-touch-callout: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}#cb-enh-theater-controls-area {position: absolute;width: 100%;heigth: 50px;}.vjs-menu-button-popup .vjs-menu .vjs-menu-content {max-height: 20em !important;}div.vjs-menu-button.vjs-menu-button-popup.vjs-control.vjs-button .vjs-menu-content {border-radius: 8px 8px 0 0;}#TheaterModePlayer #volume-high, #TheaterModePlayer #volume-mute, .theater-video-controls {-webkit-user-drag: none;-webkit-app-region: no-drag;user-drag: none;app-region: no-drag;-webkit-touch-callout: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}.cb-enh-grid-size-selector-img {float: right;position: relative;margin: 0;display: block;width: 30px;margin-left: 0px;cursor: pointer;}.cb-enh-grid-size-selector-img:not(.cb-enh-grid-size-selector-more-img) {display: none;}.cb-enh-grid-size-selector-img path {fill: #ff804a;}.darkmode .cb-enh-grid-size-selector-img path {fill: #7ab9ff;}.main-content-wrapper:has(.top-section:not(.roomPage)) .cb-enh-grid-size-selector-img:not(.cb-enh-grid-size-selector-more-img) {display: block;}#cb-enh-multi-size-selector .cb-enh-grid-size-selector-img {display: block;}#cb-enh-multi-size-selector .cb-enh-grid-size-selector-img {filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.1));}#cb-enh-multi-size-selector .cb-enh-multi-custom-grid-selector {width: auto;padding: 2px 4px;border: 1px solid currentColor;background: transparent;color: inherit;font-size: 13px;}.darkmode #cb-enh-multi-size-selector .cb-enh-grid-size-selector-img,.cb-enh-multi-fullscreen #cb-enh-multi-size-selector .cb-enh-grid-size-selector-img {filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.25));}.MoreRooms .list .roomCard a img {width: 100%;height: 100%;}.cb-enh-grid-size-selector-more-img {margin-top: -20px;margin-right: -10px !important;margin-left: 8px;}.cb-enh-add-cam-icon, .cb-enh-room-more-icon {float: right;cursor: pointer;position: absolute;bottom: -4px;}.cb-enh-add-cam-icon {right: 20px;}.cb-enh-room-more-icon {right: -2px;}.cb-enh-room-preview-icon {display: block;float: left;cursor: pointer;position: absolute;margin-top: 1px;margin-left: 1px;top: 0;left: 0;width: 23.25px;height: 23.25px;filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 1));z-index: 1;opacity: 0;}.RoomCard:hover[data-show-big-preview-icon] .cb-enh-room-preview-icon,.roomCard:hover[data-show-big-preview-icon] .cb-enh-room-preview-icon {opacity: 0.95;}.sub-info li.cams {width: 100% !important;}.darkmode .cb-enh-add-cam-icon svg path, .darkmode #cb-enh-multi-tip-add-icon svg path, .cb-enh-multi-fullscreen #cb-enh-multi-tip-add-icon svg path,.darkmode .cb-enh-room-more-icon svg path {fill: #fbfbfb;}.cb-enh-big-window {position: fixed;z-index: 99999999;top: 0;left: 0;width: 100%;display: none;justify-content: center;}.cb-enh-big-window a {text-decoration: underline;color: black;}.darkmode .cb-enh-big-window a {color: white;}.cb-enh-big-window-close-icon {cursor: pointer;position: absolute;top: 4px;right: 4px;}.cb-enh-big-window-close-icon svg path {fill: #e80000;}.cb-enh-big-window-inner {position: fixed;width: 100%;max-width: 820px;height: 650px;border-radius: 4px;border: 1px solid #acacac;background-color: #fbfbfb;padding: 10px;top: 17%;top: calc(50% - 650px/2);top: max(0px, calc(50% - 650px/2 - 82px/2));}.darkmode .cb-enh-big-window-inner {border: 1px solid #121820;background-color: #17202a;color: #f7f7f7;}.cb-enh-big-window-content {overflow: auto;width: 100%;height: 85%;height: calc(89% - 25px);font-size: 14px;line-height: 20px;color-scheme: light dark;}.cb-enh-big-window-header {font-size: 22px;font-weight: bold;margin-top: 5px;margin-bottom: 8px;}.cb-enh-big-window-subheader {font-size: 18px;font-weight: bold;}#cb-enh-page-overlay {display: none;position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: #0000008a;z-index: 99999996;}body > #base.cb-enh-base-blurred {-moz-filter: blur(10px);-ms-filter: blur(10px);-o-filter: blur(10px);filter: blur(10px);transform: translateZ(0);pointer-events: none;}html.cb-enh-html-noscroll {overflow: hidden;}#nav {overflow: visible !important;}#cb-enh-toggle-settings {text-transform: uppercase;}#cb-enh-settings-window input[type="checkbox"], #cb-enh-settings-window label {cursor: pointer;}#cb-enh-settings-window input[type="checkbox"] {margin: 0;}#cb-enh-settings-window label:not(:has(span)),#cb-enh-settings-window label > span,#cb-enh-video-controls-modal label,.cb-enh-multi-info label > span {padding-left: 4px;}#cb-enh-settings-footer {position: absolute;bottom: 10px;}.cb-enh-st-blocklist-remove-icon {display: inline-block;cursor: pointer;vertical-align: middle;width: 17px;height: 17px;}.cb-enh-st-blocklist-remove-icon svg {width: 100%;height: 100%;}.cb-enh-st-blocklist-remove-icon svg path {fill: #e80000;}.cb-enh-st-blocklist-username {height: 20px;}#cb-enh-more-rooms-size-selectors {display: none;position: relative;top: 24px;right: 20px;}#roomTabs:has(.MoreRooms[style*="display: block;"]) #cb-enh-more-rooms-size-selectors {display: block;}#cb-enh-notification {display: none;position: fixed;z-index: 99999995;bottom: 10px;width: 100%;justify-content: center;pointer-events: none;}#cb-enh-notification-inner {width: 100%;max-width: 820px;height: 35px;border-radius: 4px;border: 1px solid #acacac;background-color: #fbfbfb;padding: 8px;pointer-events: all;font-size: 16px;opacity: 0.95;}.darkmode #cb-enh-notification-inner {border: 1px solid #121820;background-color: #0d1318;color: #f7f7f7;}.cb-enh-room-more-menu {display: none;border-radius: 2px;position: absolute;width: 120px;height: 26px;font-size: 13px;border: 1px solid #acacac;background-color: #f0f1f1;color: black;z-index: 99999997;list-style: none;padding: 0;margin: 0;}.darkmode .cb-enh-room-more-menu {border: 1px solid #2d3e50;background-color: #202b38;color: #fbfbfb;}.cb-enh-room-more-menu li {cursor: pointer;padding: 4px;}.cb-enh-room-more-menu li:hover {background-color: #e4e4e4;}.darkmode .cb-enh-room-more-menu li:hover {background-color: #101f30;}.cb-enh-more-menu-hide-cc {display: none;}.darkmode .roomElementAnchor.isHighlighted {background-color: #193655 !important;}.darkmode .roomElementAnchor:hover {color: #00b6ff !important;}.darkmode .roomCard .sub-info {color: #a1a1a1 !important;}#`+gtrrft('mspr')+`, .`+gtrrft('strpb')+` {clear: both;margin-top: 10px;font-size: 18px;color: white;border: 1px solid #496b91;padding: 5px;border-radius: 4px;background-color: #003168;max-width: 750px;line-height: 21px;cursor: pointer;transition: 0.2s background-color;font-weight: bold;}.darkmode #`+gtrrft('mspr')+`, .darkmode .`+gtrrft('strpb')+` {background-color: #012247;}#`+gtrrft('mspr')+`:hover, .`+gtrrft('strpb')+`:hover {background-color: #003979;}#` + gtrrft('blso') + ` {display: flex;position: fixed;z-index: 99999998;top: 0;left: 0;width: 100%;display: flex;justify-content: center;}#` + gtrrft('blso') + `.cb-enh-hidden {display: none;}#` + gtrrft('bls') + ` {width: 600px;position: fixed;bottom: 40px;z-index: 9999999;max-width: 820px;position: fixed;width: 100%;max-width: 820px;padding: 8px;}body.cb-enh-chat-hide-notices div[data-testid="chat-message"]:has(.roomNotice:not(.isTip):not(.titleChange):not(.bright-background)) {display: none !important;}body.cb-enh-chat-hide-subject-notices div[data-testid="chat-message"]:has(.roomNotice.titleChange) {display: none !important;}body.cb-enh-chat-hide-tips div[data-testid="chat-message"]:has(.isTip) {display: none !important;}body.cb-enh-chat-hide-greys div[data-testid="chat-message"]:has(.defaultUser) {display: none !important;}/*.cb-enh-multi-cam-img-wrap {display: none;}.cb-enh-multi-cam-wrap2 {background-color: unset !important;}*/#cb-enh-multi-cams {display: grid;grid-auto-flow: row dense;grid-auto-rows: 1px;grid-template-columns: repeat(` + gMultiGridColumnCount + `, 1fr);grid-template-rows: repeat(` + gMultiGridColumnCount + `, 1px);}.cb-enh-multi-rtl #cb-enh-multi-cams {direction: rtl;}.cb-enh-multi-rtl .cb-enh-multi-cam {direction: ltr;}.cb-enh-multi-rtl .cb-enh-multi-resize-handle {right: unset;left: 0;}.cb-enh-vcontrols-block-left {padding-left: 0 !important;}.cb-enh-multi-rtl .cb-enh-vcontrols-block-left {padding-left: 8px !important;}.cb-enh-multi-rtl .cb-enh-vcontrols-block-right {padding-right: 0px !important;}.cb-enh-vcontrols-block.theater-overlay {column-gap: 8px;}.cb-enh-multi-cam:has(#cb-enh-duration) .cb-enh-vcontrols-block.theater-overlay {margin-left: -48px;}#cb-enh-multi-content.cb-enh-multi-fullscreen-cam .cb-enh-vcontrols-block.theater-overlay {column-gap: 24px;}.langs {padding-bottom: 10px !important;}.cb-enh-footer {padding-bottom: 10px;}#cb-enh-room-preview-video, .cb-enh-room-preview-frame {position: absolute;inset: 0;width: 100%;height: 100%;object-fit: cover;border-bottom-left-radius: 0;border-bottom-right-radius: 0;}.cb-enh-room-preview-frame {pointer-events: none;z-index: 1;}.cb-enh-room-preview-thumbnail {transition: none !important;}a.FollowedDropdown__room-link #cb-enh-room-preview-video,a.FollowedDropdown__room-link .cb-enh-room-preview-frame {height: calc(100% - 36px);pointer-events: none;}.RoomCardThumbnail__image, .room_thumbnail_container, a.FollowedDropdown__room-link {position: relative;}.FollowedDropdown__room {position: relative;}.FollowedDropdown__room .cb-enh-add-cam-icon {right: 4px;bottom: 2px;}#cb-enh-room-preview {display: none;position: fixed;width: 500px;height: 281.25px;bottom: 10px;right: 10px;background: black;border-radius: 12px;z-index: 9999;pointer-events: none;background-color: #f0f1f1;border: 1px solid #acacac;}.darkmode #cb-enh-room-preview {background-color: #202c39;border-color: #17202a;}#cb-enh-room-preview-preview {position: relative;}#cb-enh-room-preview video, #cb-enh-room-preview img {float: left;position: absolute;top: 0;left: 0;width: 100%;height: 100%;border-top-left-radius: 12px;border-top-right-radius: 12px;}#cb-enh-room-preview img {display: none;}#cb-enh-room-preview-info-top {border-bottom: 1px solid #acacac;height: 24px;}.darkmode #cb-enh-room-preview-info-top {border-bottom-color: #2d3e50;}#cb-enh-room-preview-username {font-size: 24px;color: #0c6a93;padding-bottom: 6px;float: left;}#cb-enh-room-preview-info {padding: 8px;}#cb-enh-room-preview-info-right {float: right;}#cb-enh-room-preview-info-age {font-size: 14px;}#cb-enh-room-preview-info-right > div, #cb-enh-room-preview-info-right .thumbnail_flag {float: left;}#cb-enh-room-preview-info-gender span {background-size: contain !important;width: 14px;height: 14px;display: inline-block;margin-left: 4px;}#cb-enh-room-preview-info-gender .genderf {background: url(https://web.static.mmcdn.com/images/ico-female.svg) no-repeat 100% 0;}#cb-enh-room-preview-info-gender .genderc {background: url(https://web.static.mmcdn.com/images/ico-couple.svg) no-repeat 100% 0;}#cb-enh-room-preview-info-gender .genderm {background: url(https://web.static.mmcdn.com/images/ico-male.svg) no-repeat 100% 0;}#cb-enh-room-preview-info-gender .genders {background: url(https://web.static.mmcdn.com/images/ico-trans.svg) no-repeat 100% 0;}.darkmode #cb-enh-room-preview-username {color: #68b5f0;}#cb-enh-room-preview-subject {clear: both;margin-top: 2px;font-size: 13px;overflow: hidden;max-height: 16px;}#cb-enh-room-preview-location {background: url(https://web.static.mmcdn.com/images/location.svg) no-repeat 0 50%;background-size: auto;padding-left: 18px;}.darkmode #cb-enh-room-preview-location {background: url(https://web.static.mmcdn.com/images/location_white.svg) no-repeat 0 50%;}#cb-enh-room-preview-caminfo {background: url(https://web.static.mmcdn.com/images/cam.svg) no-repeat 0 50%;background-size: auto;padding-left: 18px;}.darkmode #cb-enh-room-preview-caminfo {background: url(https://web.static.mmcdn.com/images/cam_white.svg) no-repeat 0 50%;}#cb-enh-pip {display: inline-flex;position: relative;align-items: center;justify-content: center;min-width: 32px;user-select: none;pointer-events: auto;}#cb-enh-pip > .video-controls-tooltip, .cb-enh-video-controls-tooltip {position: absolute;display: block;opacity: 0;bottom: calc(100% + 5px);left: 50%;transform: translateX(-50%);border-radius: 4px;background-color: rgba(0, 0, 0, 0.92);padding: 8px 16px;text-align: center;font-size: 13px;color: rgb(255, 255, 255);width: max-content;max-width: 150px;transition: inherit;pointer-events: none;visibility: hidden;}#cb-enh-pip > .video-controls-tooltip > p, .cb-enh-video-controls-tooltip > p {display: inline;}#cb-enh-pip svg {opacity: 0.8;-webkit-transform: scale(0.9);transform: scale(0.9);-webkit-transition-property: all;-webkit-transition-duration: 150ms;-webkit-transition-timing-function: ease;transition-property: all;transition-duration: 150ms;transition-timing-function: ease;}#cb-enh-pip:hover svg {opacity: 1.0;-webkit-transform: scale(1.1);transform: scale(1.1);}#cb-enh-pip.cb-enh-control-animating svg,.cb-enh-vcontrols-btn-fs.cb-enh-control-animating img,:is(#full-screen-icon, #theater-mode-icon).cb-enh-control-animating,:is(#full-screen-icon, #theater-mode-icon).cb-enh-control-animating :is(img, svg) {will-change: transform, opacity;}.cb-enh-control-animating :is(.video-controls-tooltip, .cb-enh-video-controls-tooltip, .cb-enh-video-controls-seekbar-tooltip) {will-change: opacity;}.cb-enh-seekbar.cb-enh-control-animating .cb-enh-seekbar__thumb {will-change: transform;}#cb-enh-pip:hover .video-controls-tooltip,#cb-enh-multi-content.cb-enh-multi-fullscreen-cam .cb-enh-vcontrols-btn:hover .video-controls-tooltip:not(.cb-enh-video-controls-tooltip-no-fs),#cb-enh-multi-content:not(.cb-enh-multi-fullscreen-cam) .cb-enh-vcontrols-btn:hover .video-controls-tooltip,#TheaterModeRoomContents .cb-enh-vcontrols-btn:hover .cb-enh-video-controls-tooltip {opacity: 1 !important;visibility: visible !important;}.cb-enh-vcontrols-btn-fs img {opacity: 0.8;}.cb-enh-vcontrols-btn:hover .cb-enh-vcontrols-btn-img,.cb-enh-vcontrols-btn:hover svg,.cb-enh-vcontrols-btn:hover span {opacity: 1 !important;}.cb-enh-vcontrols-btn-fs:hover img {transform: scale(1.15);}.cb-enh-vcontrols-btn-fs:hover img[src$="/exit-fullscreen.svg"] {transform: scale(0.85);}.cb-enh-vcontrols-btn.slider:hover .cb-enh-vcontrols-slider-part {opacity: 1 !important;}div[data-testid="quality-option"], .video-controls-tooltip {-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}.thumbnail_flag {min-width: 16.01px;}#animate_thumbnails_form .tooltip {display: none !important;}.FollowRecommendedRoomlist .RoomCardGrid--oneRow {grid-template-rows: unset !important;grid-auto-rows: unset !important;grid-gap: 0.6em 0.75em !important;}.FollowRecommendedRoomlist {background-color: unset !important;border-top: 2px dashed #acacac !important;border-radius: unset !important;}.darkmode .FollowRecommendedRoomlist {border-color: #2d3e50 !important;}.FollowRecommendedRoomlist h2 {padding-top: 12px !important;padding-left: 1px !important;margin-bottom: 12px;}.darkmode .FollowRecommendedRoomlist h2 {color: #68b5f0 !important;}.followed_online_offline {padding: 0 !important;}.followed_online_offline p {margin: 0 !important;}.followed_online_offline .title[data-testid="online-filter-title"] {margin-left: 18px !important;}.followed_online_offline .title.bold {font-weight: bold !important;}#cb-enh-video-controls-modal {pointer-events: all !important;}#cb-enh-video-controls-modal-content input[type="checkbox"],#cb-enh-video-controls-modal-content input[type="button"] {cursor: pointer;}.cursor-pointer {cursor: pointer;}.FollowedDropdown {overflow-x: hidden !important;color-scheme: light dark;}div[data-testid="roomSubjectContainer"] {min-height: 34px !important;}.top-section {padding-top: 8px !important;padding-bottom: 0 !important;}.BaseRoomContents {margin-top: 0 !important;}body.cb-enh-small-margins .main-content-wrapper:has(.top-section.roomPage) {padding-left: 0 !important;padding-right: 0 !important;}body.cb-enh-small-margins .BaseRoomContents {margin-left: 0 !important;}body.cb-enh-small-margins #theatermode-root {margin-right: 0 !important;}body.cb-enh-small-margins:has(#theater-mode-icon[aria-label="Default View"]) #TheaterModeRoomContents {margin: 0 !important;padding: 0 !important;border: 0 !important;}body.cb-enh-small-margins:has(#theater-mode-icon[aria-label="Default View"]) #VideoPanel {min-width: calc(100% + 10px) !important;border: 0 !important;}body.cb-enh-gb-f:has(.GenderNav__button.active[data-testid="gender-nav-"]):not(:has(a#following.HeaderNavBar__link--active)) .RoomCard:has(span.genderf) {display: none !important;}body.cb-enh-gb-m:has(.GenderNav__button.active[data-testid="gender-nav-"]):not(:has(a#following.HeaderNavBar__link--active)) .RoomCard:has(span.genderm) {display: none !important;}body.cb-enh-gb-c:has(.GenderNav__button.active[data-testid="gender-nav-"]):not(:has(a#following.HeaderNavBar__link--active)) .RoomCard:has(span.genderc) {display: none !important;}body.cb-enh-gb-t:has(.GenderNav__button.active[data-testid="gender-nav-"]):not(:has(a#following.HeaderNavBar__link--active)) .RoomCard:has(span.genders) {display: none !important;}body.cb-enh-gb-f .FollowRecommendedRoomlist .RoomCard:has(span.genderf) {display: none !important;}body.cb-enh-gb-m .FollowRecommendedRoomlist .RoomCard:has(span.genderm) {display: none !important;}body.cb-enh-gb-c .FollowRecommendedRoomlist .RoomCard:has(span.genderc) {display: none !important;}body.cb-enh-gb-t .FollowRecommendedRoomlist .RoomCard:has(span.genders) {display: none !important;}.darkmode .FollowedDropdown__room-username--highlighted {background: #30455d !important;}.HeaderHomeLink {outline: none;}body.darkmode-cenhf form .fieldset_main th label {color: #ececec;}body.darkmode-cenhf #main .content_body a, body.darkmode-cenhf #main .form_body a {color: #f3f3f3 !important;}body.darkmode-cenhf #main a.medium_popup_link > div > p {color: #f3f3f3 !important;}body.darkmode-cenhf #main .content_body h1 {color: #ececec !important;}body.darkmode-cenhf #main h1 {color: #ececec !important;}.HeaderTopRow__search-bar > div.RichTooltip[role="tooltip"] {display: none !important;}#age_gate_overlay {display: none !important;}body.cb-enh-hvl #VideoPanel .cbLogo {display: none !important;}body.cb-enh-arl #ChatTabContents .rulesModal {visibility: hidden !important;}body.cb-enh-arl #ChatTabContents:has(.rulesModal) div[style^="position: fixed"] {visibility: hidden !important;}.cb-enh-tooltip {width: 260px;border-width: 1px;position: absolute;border-style: solid;border-radius: 4px;font-size: 14px;padding: 8px 8px 8px 8px;display: none;z-index: 15;line-height: 22px;box-shadow: rgba(0, 0, 0, 0.08) 0px 4px 16px;background-color: #fdfdfd;border: 1px solid #acacac;}.darkmode .cb-enh-tooltip {background-color: #19222c;border-color: #003061;color: white;}#cb-enh-record-warn-modal .cb-enh-big-window-inner {height: 70px;}#cb-enh-duration {display: none;position: relative;align-items: center;justify-content: center;min-width: 32px;user-select: none;pointer-events: none;margin: 0px 6px;margin-top: 4px;color: white;}#cb-enh-duration > span {background-color: #00000094;border-radius: 4px;padding: 4px;}body.cb-enh-drt #cb-enh-duration {display: inline-flex;}.cb-enh-checkbox {appearance: none;border-radius: 2px;border-style: solid;border-width: 1px;display: inline-block;height: 16px;width: 16px;position: relative;overflow: visible;-webkit-tap-highlight-color: transparent;cursor: pointer;vertical-align: text-top;background-color: #ffffff;border-color: #a8a8a8;margin: 0;}.cb-enh-checkbox:hover {background-color: #f6f6f6;border-color: #909090;}.darkmode .cb-enh-checkbox {background-color: #17202a;border-color: #2d3e50;}.darkmode .cb-enh-checkbox:hover {background-color: #121a22;border-color: #2d3e50;}.cb-enh-checkbox:checked {background-color: #0c6a93 !important;background: url(https://web2.static.mmcdn.com/tsdefaultassets/settings-checkmark-white.svg) no-repeat center center / 60%, #0c6a93;border-color: #033e58;}.cb-enh-checkbox:disabled {background-color: #f8f9fa;border-color: #efefef;}.darkmode .cb-enh-checkbox:disabled {background-color: #202c39;border-color: #2d3e50;}.cb-enh-btn {appearance: none;--bg: #004ca1;color: white;width: auto;position: relative;overflow: hidden;-webkit-tap-highlight-color: transparent;font-family: UbuntuMedium, Helvetica, Arial, sans-serif;font-size: 12px;padding: 3px 8px 2px;border-radius: 3px;cursor: pointer;margin-right: 5px;background-color: var(--bg);text-transform: uppercase;border-bottom-width: 0.8px;border-left-width: 0.8px;border-right-width: 0.8px;border-top-width: 0.8px;border: 1px solid var(--bg);}.darkmode .cb-enh-btn {--bg: #4f4f4f;color: white;}.cb-enh-slider {color-scheme: light dark;}@-moz-document url-prefix() {.cb-enh-slider {appearance: none;background: transparent;border-radius: 3px;}}.darkmode .cb-enh-slider {accent-color: #0087f1;}.cb-enh-slider::-moz-range-track {height: 6px;background: #efefef;border-radius: 3px;}body:not(.darkmode) .cb-enh-slider::-moz-range-track {border: 1px solid #b2b2b2;}.darkmode .cb-enh-slider::-moz-range-track {background: #3b3b3b;}.cb-enh-slider::-moz-range-thumb {width: 16px;height: 16px;border-radius: 50%;background: #0087f1;border: none;cursor: pointer;}.cb-enh-slider::-moz-range-progress {height: 6px;background: #0087f1;border: none;border-radius: 3px;}.cb-enh-input-text {border-width: 1px;border-style: solid;border-radius: 4px;padding: 2px 4px;line-height: 18px;margin-top: 4px;}.darkmode .cb-enh-input-text {background-color: #202c39;color: white;border-color: #3b3b3b;}.cb-enh-input-select {cursor: pointer;background-color: #ffffff;}.darkmode .cb-enh-input-select {background-color: #202c39;border-color: #3b3b3b;color-scheme: dark !important;accent-color: #0087f1 !important;width: 100px;}.darkmode .cb-enh-input-select option {color-scheme: dark !important;accent-color: #0087f1 !important;}.darkmode .cb-enh-input-select, .darkmode .cb-enh-input-select::picker(select) {appearance: base-select;}.darkmode .cb-enh-input-select::picker(select) {border-color: #3b3b3b;border-radius: 4px;background-color: #202c39;width: 100px;}.cb-enh-input-select {outline: none;border-radius: 4px;}.darkmode .cb-enh-input-select {color: #f7f7f7;}.cb-enh-hidden {display: none;}#cb-enh-manager-feature-error {display: none;font-size: 17px;line-height: 20px;}#cb-enh-manager-feature-error a {color: white;text-decoration: underline;}body:not(.darkmode) .AnonUserDropdown__trigger svg path {fill: #0c6a93;}#cb-enh-schedule {max-width: 1000px;margin: unset;}#cb-enh-schedule-day-list {display: flow-root;}.cb-enh-schedule-title {margin-top: 10px;}.cb-enh-schedule-title h2 {float: left;display: inline-block;vertical-align: middle;text-transform: uppercase;font-size: 17px;font-weight: bold;margin: 0;}.cb-enh-schedule-title span {float: left;vertical-align: middle;display: inline-block;font-size: 11px;margin-left: 5px;}.cb-enh-schedule-days {width: 70%;}.cb-enh-schedule-day {margin-top: 4px;clear: both;}.cb-enh-schedule-day .cb-enh-schedule-dtxt {float: right;text-align: right;padding-right: 4px;padding-top: 2px;font-size: 12px;line-height: 27px;}.cb-enh-schedule-hour-wrap {width: calc(3%);float: right;margin-left: 1px;margin-top: 1px;}.cb-enh-schedule-hour {display: block;float: left;width: 100%;padding-bottom: 100%;}cb-enh-schedule-hour-wrap:hover .cb-enh-schedule-hour, .cb-enh-schedule-hour-current {box-shadow: inset 0px 0px 0px 2px #2d2d2d;}.darkmode .cb-enh-schedule-hour-wrap:hover .cb-enh-schedule-hour, .darkmode .cb-enh-schedule-hour-current {box-shadow: inset 0 0 0 2px white;}.cb-enh-schedule-hw-tl .cb-enh-schedule-hour {border-top-left-radius: 5px;}.cb-enh-schedule-hw-tr .cb-enh-schedule-hour {border-top-right-radius: 5px;}.cb-enh-schedule-hw-bl .cb-enh-schedule-hour {border-bottom-left-radius: 5px;}.cb-enh-schedule-hw-br .cb-enh-schedule-hour {border-bottom-right-radius: 5px;}.cb-enh-schedule-hour-label {float: right;width: calc(3% + 1px);font-size: 12px;transform: rotate(-50deg);margin-bottom: 5px;margin-top: 10px;white-space: nowrap;}.cb-enh-schedule-spacer-wrap { float: right; margin-left: 1px; margin-top: 1px; width: 12.5%;}.cb-enh-schedule-spacer {display: block; float: right; width: 100%; padding-bottom: 24%;}@media (max-width: 1000px){.cb-enh-schedule-day .cb-enh-schedule-dtxt {line-height: 10px !important;}}@media (max-width: 650px){.cb-enh-schedule-spacer {display: none;}}.cb-enh-schedule-tooltip-wrap {position: fixed;pointer-events: none;display: none;}.cb-enh-schedule-tooltip-ar {width: 0;height: 0;border-left: 6px solid transparent;border-right: 6px solid transparent;border-bottom: 6px solid #151515;margin: 0 auto;margin-top: 3px;}.cb-enh-schedule-tooltip-inner {height: 100%;background-color: #151515;color: white;border-radius: 4px;font-size: 12px;padding: 2px;padding-left: 6px;padding-right: 6px;}.`+gtrrft('sp')+` {text-align: center;margin-top: 15px;}.cb-enh-clear {clear: both;}.darkmode .cb-enh-schedule, #cb-enh-schedule a {color: white;}#cb-enh-schedule a {color: #e80000 !important;}.cb-enh-row {display: none;font-size: 14px;font-weight: normal;line-height: 15px;vertical-align: top;text-align: left;}.cb-enh-row > td.label {padding-bottom: 9px;font-family: UbuntuMedium, Arial, Helvetica, sans-serif;height: 16px;}.cb-enh-row > td.cb-enh-row-value {font-size: 14px;line-height: 16px;font-family: UbuntuRegular, Arial, Helvetica, sans-serif;}.content_body.fanclub_container .frame.fanclub_body img[data-testid="fanclub-promo-image"] {border-radius: 8px;}.darkmode .content_body.fanclub_container .frame.fanclub_body {background-color: #1a2531;border-color: #004795;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc {color: white;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .fanclub_benefits td {color: white;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .fanclub_benefits th > strong {color: white;font-weight: bold;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .fanclub_benefits .blue_background {background-color: #00408740;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .fanclub_benefits .blue_background > td {color: white !important;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .fanclub_benefits tr:not(.blue_background):not(:first-child) {background-color: #003a7b;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .fanclub_actions .big_form_button--blue:hover {background-color: #ff6300;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .support_message .highlight {background-color: #002249;border-color: #004795;border-radius: 4px;}.darkmode .content_body.fanclub_container .frame.fanclub_body .fanclub_desc .support_message .highlight .h_header {color: white;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame {background-color: #00408740;border-color: #004795;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame .header {background-color: #00408740;color: white;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame .footer {color: white;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame .basic_user {color: #dfdfdf;font-weight: normal;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame .premium_user {color: white;font-weight: bold;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .price {color: white;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame .blue_background {background-color: #00408740;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame tr:not(.blue_background):not(:first-child) {background-color: #003a7b;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame td {color: white;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper .frame td .feature_detail {color: white;}.darkmode #main.supporter_upgrade #supporter_upgrade_content .content_wrapper h2 {color: #009aff;}#cb-enh-theater-video-controls-btn {display: none;position: relative;align-items: center;user-select: none;justify-content: center;pointer-events: auto;}#cb-enh-theater-video-controls-btn span {color: white;font-size: 13px;padding: 5px;display: inline-block;text-align: left;}#cb-enh-theater-video-controls-btn span > p {display: inline;}#cb-enh-theater-video-controls-btn:hover > * {opacity: 1 !important;}#cb-enh-settings-group-selector-group-selector {border: 1px solid #4093f9;background-color: #b6d8ff;border-radius: 4px;display: inline-block;margin-bottom: 4px;}.darkmode #cb-enh-settings-group-selector-group-selector {border: 1px solid #000914;background-color: #112031;}.cb-enh-settings-group-selector-group {display: inline-block;cursor: pointer;padding-top: 4px;padding-bottom: 4px;padding-left: 8px;padding-right: 8px;font-size: 16px;}.cb-enh-settings-group-selector-group.cb-enh-active, .cb-enh-settings-group-selector-group:hover {background-color: #6cb0fe;}.darkmode .cb-enh-settings-group-selector-group.cb-enh-active, .darkmode .cb-enh-settings-group-selector-group:hover {background-color: #0e3460;}.cb-enh-settings-group-selector-group:first-child {border-top-left-radius: 4px;border-bottom-left-radius: 4px;}.cb-enh-settings-group-selector-group:last-child {border-top-right-radius: 4px;border-bottom-right-radius: 4px;}.cb-enh-settings-group {display: none;}.cb-enh-settings-group.cb-enh-active {display: block;}.cb-enh-settings-group[data-group="info"] {cursor: pointer;}.cb-enh-settings-group[data-group="export"] {margin-top: 5px;}#cb-enh-settings-export-info {margin-top: 5px;}.MoreRooms .list {position: relative;display: grid;grid-gap: .6em .75em;grid-template-columns: repeat(auto-fill, minmax(174px, 1fr));margin: 0;overflow: hidden;padding: 0;padding-right: 2px;}.MoreRooms .list .roomCard {border: 1px solid var(--cbr-RoomCard-border-color);border-radius: 4px;-moz-border-radius: 4px;-webkit-border-radius: 4px;list-style: none;margin: 0;overflow: hidden;position: relative;width: 100%;background-color: var(--cbr-RoomCard-background-color);}.MoreRooms .list .roomCard .room_thumbnail { display: block; height: auto; width: 100%;}.GenderNav__scrollContainer:has(a.cb-enh-gender-nav-btn[data-testid="gender-nav-"]) button.GenderNav__button[data-testid="gender-nav-"],.GenderNav__scrollContainer:has(a.cb-enh-gender-nav-btn[data-testid="gender-nav-f"]) button.GenderNav__button[data-testid="gender-nav-f"],.GenderNav__scrollContainer:has(a.cb-enh-gender-nav-btn[data-testid="gender-nav-m"]) button.GenderNav__button[data-testid="gender-nav-m"],.GenderNav__scrollContainer:has(a.cb-enh-gender-nav-btn[data-testid="gender-nav-c"]) button.GenderNav__button[data-testid="gender-nav-c"],.GenderNav__scrollContainer:has(a.cb-enh-gender-nav-btn[data-testid="gender-nav-t"]) button.GenderNav__button[data-testid="gender-nav-t"] {display: none !important;}a.cb-enh-gender-nav-btn {color: #525252 !important;text-decoration: none !important;}.darkmode a.cb-enh-gender-nav-btn {color: white !important;}a.cb-enh-gender-nav-btn.active {color: white !important;}.darkmode .accounts_list_followers .room_list_room {background-color: #202c39;border-color: #2d3e50;}.darkmode .accounts_list_followers .room_list_room .age,.darkmode .accounts_list_followers .room_list_room .sub-info,.darkmode .accounts_list_followers .room_list_room .subject {color: #f7f7f7;}.cb-enh-nss .BaseRoomContents .dismissibleMessage:has(a[href^="https://www.surveymonkey.com/"]),.cb-enh-nss .fanclub_container .feedback_notice,.cb-enh-nss a.feedbackLink,.cb-enh-nss div[data-testid="suggest-other-payment-methods-btn"],.cb-enh-nss #supporter_upgrade_content .upgrade_footer_text:has(a[href^="https://www.surveymonkey.com/"]),.cb-enh-nss div[data-testid="feedback-link"],.cb-enh-nss li:has(a[data-testid="feedback"]),.cb-enh-nss a[data-testid="feedback"] {display: none !important;}.cb-enh-vcontrols-btn img, .cb-enh-vcontrols-btn:not(.cb-enh-vcontrols-btn-pause) svg {width: 20px;height: 20px;}.cb-enh-seekbar {--cb-enh-seekbar-height: 4px;--cb-enh-seekbar-thumb-size: 12px;--cb-enh-seekbar-progress: 42%;--cb-enh-seekbar-buffer: 68%;--cb-enh-seekbar-track-color: rgba(255, 255, 255, 0.3);--cb-enh-seekbar-buffer-color: rgba(255, 255, 255, 0.5);--cb-enh-seekbar-progress-color: #fff;position: absolute;width: calc(100% - 16px);height: 20px;cursor: pointer;display: flex;align-items: center;bottom: 30px;left: 0;margin-left: 8px;pointer-events: auto;}.cb-enh-seekbar__track {position: relative;width: 100%;height: var(--cb-enh-seekbar-height);border-radius: 999px;background: var(--cb-enh-seekbar-track-color);}.cb-enh-seekbar__buffer,.cb-enh-seekbar__progress {position: absolute;top: 0;right: 0;left: auto;height: 100%;border-radius: inherit;pointer-events: none;}.cb-enh-seekbar__buffer {width: var(--cb-enh-seekbar-buffer);background: var(--cb-enh-seekbar-buffer-color);}.cb-enh-seekbar__progress {width: var(--cb-enh-seekbar-progress);background: var(--cb-enh-seekbar-progress-color);}.cb-enh-seekbar__thumb {position: absolute;top: 50%;right: var(--cb-enh-seekbar-progress);left: auto;width: var(--cb-enh-seekbar-thumb-size);height: var(--cb-enh-seekbar-thumb-size);border-radius: 50%;background: var(--cb-enh-seekbar-progress-color);transform: translate(50%, -50%) scale(0);transition: transform 120ms ease;box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);}.cb-enh-seekbar:hover .cb-enh-seekbar__thumb,.cb-enh-seekbar:focus-within .cb-enh-seekbar__thumb {transform: translate(50%, -50%) scale(1);}.cb-enh-seekbar:hover .cb-enh-seekbar__track {height: 5px;}.cb-enh-seekbar__track,.cb-enh-seekbar__thumb {transition:height 120ms ease,transform 120ms ease;}.cb-enh-video-controls-seekbar-tooltip {position: absolute;display: block;opacity: 0;bottom: calc(100% + -4px);left: 50%;transform: translateX(-50%);border-radius: 4px;background-color: rgba(0, 0, 0, 0.92);padding: 4px 8px;text-align: center;font-size: 11px;color: rgb(255, 255, 255);width: max-content;max-width: 150px;transition: inherit;pointer-events: none;visibility: hidden;text-transform: uppercase;}.cb-enh-seekbar:hover .cb-enh-video-controls-seekbar-tooltip {opacity: 1;visibility: visible;}.cb-enh-video-controls-seekbar-tooltip-preview {max-width: unset;}.cb-enh-video-controls-seekbar-tooltip canvas {display: none;}.cb-enh-video-controls-seekbar-tooltip-preview canvas {width: 200px;height: 112.5px;padding-top: 4px;padding-bottom: 4px;display: block;}.cb-enh-video-controls-seekbar-tooltip, .theater-video-controls {opacity: 1;visibility: visible;}*/.cb-enh-video-controls-seekbar-tooltip.cb-enh-unseekable {opacity: 0.725 !important;}.cb-enh-rewinded #cb-enh-duration {pointer-events: auto;cursor: pointer;}.cb-enh-rewinded .videoPlayerDiv > canvas:not(.cb-enh-rewind-video-canvas) {display: none !important;}.cb-enh-rewind-video-canvas {position: absolute;inset: 0px;width: 100%;height: 100%;display: block;}#TheaterModePlayer {overflow: visible !important;}div[data-testid="video-quality-btn"] {transition: none !important;}.cb-enh-rewinded div[data-testid="video-quality-btn"] {pointer-events: none !important;opacity: 0.5 !important;}.cb-enh-rewinded div[data-testid="volume-button"],.cb-enh-rewinded div[data-testid="volume-slider"] {display: none !important;}#TheaterModeRoomContents:not(.cb-enh-rewinded) .cb-enh-vcontrols-btn-vol,#TheaterModeRoomContents:not(.cb-enh-rewinded) .cb-enh-vcontrols-btn.slider {display: none !important;}.cb-enh-rewinded .cb-enh-vcontrols-btn-vol,.cb-enh-rewinded .cb-enh-vcontrols-btn.slider {display: inline-flex !important;}.footer-labels img {border-radius: 4px;}.darkmode img[data-testid="safe-labeling"],.darkmode img[data-testid="rta"] {filter: invert(95%);}`); } function capitalizeFirstLetter(val) { return String(val).charAt(0).toUpperCase() + String(val).slice(1); } function hourNumberToHumanFormat(num) { if(shouldUseTimeFormat24h()) { return ('0' + num).slice(-2) + ':00'; } const hour = num % 24; return (hour % 12 || 12) + (hour < 12 ? ' AM' : ' PM'); } function shouldUseTimeFormat24h() { const settingTimeFormat = getSetting('time-format'); if(!settingTimeFormat) { return isBrowserLocale24h(); } return settingTimeFormat === 2; } function isBrowserLocale24h() { const lang = getUserOrSiteLang(); if(typeof Intl === 'undefined') { return isLangUS( getUserOrSiteLang() ) || false; } return new Intl.DateTimeFormat(lang, { hour: 'numeric' }).formatToParts(new Date(2020, 0, 1, 13)).find(part => part.type === 'hour').value.length === 2; } const HTML_SCHEDULE = `<div id="cb-enh-schedule"><div id="cb-enh-schedule-day-list"></div><div class="cb-enh-clear"></div><div class="cb-enh-schedule-hour-list"><div class="cb-enh-schedule-spacer-wrap"><div class="cb-enh-schedule-spacer"></div></div></div><div class="cb-enh-clear `+gtrrft()+`"></div><div class="`+gtrrft('sp')+`"><p class="`+gtrrft()+`"><span class="ce-loc `+gtrrft()+`" data-ce-loc="sched_powered">Schedule powered by</span> <a rel="noopener" referrerpolicy="origin" class="`+gtrrft()+`">CamSchedule.com</a></p></div></div>`; let gElScheduleTooltip = null; let gElScheduleTooltipInner = null; let gLschedChance = null; function scheduleOnHourHover(e) { const el = e.target; const dayName = el.parentElement.lastElementChild.innerText; const elHour = el.firstElementChild; const hourStart = hourNumberToHumanFormat(elHour._h); const hourEnd = hourNumberToHumanFormat(elHour._h + 1); const txt = elHour._p + '% ' + gLschedChance + ' ' + dayName + ' ' + hourStart + ' - ' + hourEnd; gElScheduleTooltipInner.innerText = txt; gElScheduleTooltip.style.display = 'block'; const rect = elHour.getBoundingClientRect(); const tooltipWidth = gElScheduleTooltip.offsetWidth; gElScheduleTooltip.style.top = rect.bottom + 'px'; gElScheduleTooltip.style.left = ( rect.left + window.scrollX + elHour.offsetWidth / 2 - tooltipWidth / 2 ) + 'px'; } function scheduleHideTooltip() { gElScheduleTooltip.style.display = 'none'; } function getLocalSchedule(scheduleData, timezoneOffsetHours) { const hoursPerWeek = 7 * 24; const localSchedule = []; for(let day = 0; day < 7; day++) { localSchedule[day] = []; for(let hour = 0; hour < 24; hour++) { const sourceHour = ((day * 24 + hour + timezoneOffsetHours) % hoursPerWeek + hoursPerWeek) % hoursPerWeek; const firstHour = Math.floor(sourceHour); const nextHour = (firstHour + 1) % hoursPerWeek; const fraction = sourceHour - firstHour; const firstPercentage = scheduleData[Math.floor(firstHour / 24)][firstHour % 24]; const nextPercentage = scheduleData[Math.floor(nextHour / 24)][nextHour % 24]; localSchedule[day][hour] = firstPercentage * (1 - fraction) + nextPercentage * fraction; } } return localSchedule; } function initSchedule(elScheduleRow, username, roomDataSecondsOnline, scheduleData) { const lang = getSiteLang(); elScheduleRow.querySelector('.cb-enh-row-value').innerHTML = HTML_SCHEDULE; const elScheduleDayList = elScheduleRow.querySelector('#cb-enh-schedule-day-list'); elScheduleRow.querySelector('a').href = 'https://camschedule.com/' + username; let schedule_weekdays = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]; if(typeof Intl !== 'undefined') { schedule_weekdays = [...Array(7)].map((_, i) => { const date = new Date(Date.UTC(2024, 0, 8 + i)); return new Intl.DateTimeFormat(lang, { weekday: 'long', timeZone: 'UTC' }).format(date); }); } for(let i = 0; i <= 6; i++) { const dayDiv = document.createElement('div'); dayDiv.className = 'cb-enh-schedule-day'; const spacerWrap = document.createElement('div'); spacerWrap.className = 'cb-enh-schedule-spacer-wrap'; spacerWrap.addEventListener('mouseenter', scheduleHideTooltip); const spacer = document.createElement('div'); spacer.className = 'cb-enh-schedule-spacer'; spacerWrap.appendChild(spacer); dayDiv.appendChild(spacerWrap); for(let i2 = 23; i2 >= 0; i2--) { const wrap = document.createElement('div'); let cls = 'cb-enh-schedule-hour-wrap'; if(i === 0 && i2 === 0) { cls += ' cb-enh-schedule-hw-tl'; } else if(i === 0 && i2 === 23) { cls += ' cb-enh-schedule-hw-tr'; } else if(i === 6 && i2 === 0) { cls += ' cb-enh-schedule-hw-bl'; } else if(i === 6 && i2 === 23) { cls += ' cb-enh-schedule-hw-br'; } wrap.className = cls; wrap.addEventListener('mouseenter', scheduleOnHourHover); const hour = document.createElement('div'); hour.className = 'cb-enh-schedule-hour'; hour._h = i2; hour._p = scheduleData[i][i2]; wrap.appendChild(hour); dayDiv.appendChild(wrap); } const txt = document.createElement('div'); txt.className = 'cb-enh-schedule-dtxt'; txt.textContent = capitalizeFirstLetter(schedule_weekdays[i]); txt.addEventListener('mouseenter', scheduleHideTooltip); dayDiv.appendChild(txt); elScheduleDayList.appendChild(dayDiv); } const elScheduleHourList = elScheduleRow.querySelector('.cb-enh-schedule-hour-list'); for(let i = 23; i >= 0; i--) { const div = document.createElement('div'); div.className = 'cb-enh-schedule-hour-label'; elScheduleHourList.appendChild(div); } scheduleUpdateHourLabels(); const now = new Date(); const userTOffset = now.getTimezoneOffset()/60; let currentDay = now.getDay(); let currentHour = now.getHours(); const elDays = document.querySelectorAll('#cb-enh-schedule .cb-enh-schedule-day'); if(!elDays) { return; } if(Math.abs(userTOffset) <= 24) { const changedSchedule = getLocalSchedule(scheduleData, userTOffset); if(roomDataSecondsOnline) { const hoursOnline = roomDataSecondsOnline / (60*60); const hoursOnlineFull = Math.round( roomDataSecondsOnline / (60*60) ); for(let i = 0; i < hoursOnlineFull; i++) { let h = currentHour - i; let d = currentDay - 1; if(currentDay === 0) { d = 6; } if(h < 0) { d--; h = h + 24; } if(d < 0) { continue; } if(changedSchedule[d][h] === 0) { changedSchedule[d][h] = 25; } } let aDay = currentDay - 1; let aHour = currentHour - hoursOnlineFull; if(aHour < 0) { aDay--; aHour = aHour + 24; } if(aDay >= 0 && hoursOnline - hoursOnlineFull >= 0.5 && changedSchedule[aDay][aHour] === 0) { changedSchedule[aDay][aHour] = 20; } } for(let i = 0; i < 7; i++) { const elDay = elDays[i]; if(!elDay) { return; } const baseColor = [202, 30, 62]; const elHours = elDay.querySelectorAll('.cb-enh-schedule-hour'); for(let i2 = 0; i2 < 24; i2++) { const elHour = elHours[23 - i2]; const perc = changedSchedule[i][i2]; let cAlpha = perc / 100; if(cAlpha < 0.07) { cAlpha = 0.07; } elHour.style.background = 'rgb(' + baseColor[0] + ' ' + baseColor[1] + ' ' + baseColor[2] + ' / ' + cAlpha + ')'; elHour._p = perc; } } } let elCurrentHour; function updateCurrentHour() { const now = new Date(); const currentDayNew = now.getDay(); const currentHourNew = now.getHours(); if(elCurrentHour && currentDayNew === currentDay && currentHour === currentHourNew) { return; } currentDay = currentDayNew; currentHour = currentHourNew; if(elCurrentHour) { elCurrentHour.classList.remove('cb-enh-schedule-hour-current'); } let currentDayIndex = currentDay - 1; if(currentDay === 0) { currentDayIndex = 6; } if(!elDays) { return; } elCurrentHour = elDays[currentDayIndex].querySelectorAll('.cb-enh-schedule-hour')[23 - currentHour]; elCurrentHour.classList.add('cb-enh-schedule-hour-current'); } clearIntervalEx(gIntvScheduleUpdateCurrentHour); gIntvScheduleUpdateCurrentHour = setIntervalRunFirst(updateCurrentHour, 1000); elScheduleDayList.addEventListener('mouseleave', function() { gElScheduleTooltip.style.display = 'none'; }); showElement(elScheduleRow); } function scheduleUpdateHourLabels() { let i = 23; document.querySelectorAll('.cb-enh-schedule-hour-label').forEach((el) => { el.innerText = hourNumberToHumanFormat(i); i--; }); } function getNextBroadcastPrediction(scheduleData) { if(!scheduleData) { return null; } const now = new Date(); const userTOffset = now.getTimezoneOffset() / 60; if(Math.abs(userTOffset) > 24) { return null; } const changedSchedule = getLocalSchedule(scheduleData, userTOffset); const currentDayIndex = (now.getDay() + 6) % 7; const currentHour = now.getHours(); let found = null; for(let offset = 1; offset <= 7 * 24; offset++) { const totalHour = currentHour + offset; const dayOffset = Math.floor(totalHour / 24); const hour = totalHour % 24; const dayIndex = (currentDayIndex + dayOffset) % 7; const perc = changedSchedule[dayIndex][hour]; if(typeof perc === 'number' && perc > 0) { found = { hoursFromNow: offset, dayOffset: dayOffset, hour: hour, percentage: perc }; break; } } if(!found) { return null; } const percentage = Math.round(found.percentage); let whenText; if(found.dayOffset === 0) { const unit = found.hoursFromNow === 1 ? getLocale('sched_hour') : getLocale('sched_hours'); whenText = getLocale('sched_in') + ' ' + found.hoursFromNow + ' ' + unit; } else if(found.dayOffset === 1) { whenText = getLocale('sched_tomorrow') + ' ' + hourNumberToHumanFormat(found.hour); } else { let weekdayName = ''; if(typeof Intl !== 'undefined') { const futureDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + found.dayOffset); weekdayName = capitalizeFirstLetter(new Intl.DateTimeFormat(getSiteLang(), { weekday: 'long' }).format(futureDate)); } whenText = weekdayName + ' ' + hourNumberToHumanFormat(found.hour); } return whenText + ' (' + percentage + '% ' + getLocale('sched_chance_short') + ')'; } const videoControlsContentHTML = `<b class="ce-loc" data-ce-loc="vid_controls">Video Controls</b><br><div><input type="checkbox" checked class="cb-enh-checkbox" id="cb-enh-video-controls-modal-show-logo"></input><label for="cb-enh-video-controls-modal-show-logo" class="cursor-pointer ce-loc" data-ce-loc="show_site_logo">Show site logo</label><br></div><div><input type="checkbox" checked class="cb-enh-checkbox" id="cb-enh-video-controls-modal-duration"></input><label for="cb-enh-video-controls-modal-duration" class="cursor-pointer ce-loc" data-ce-loc="set_duration">Show stream duration</label><br></div><div id="cb-enh-video-controls-modal-duration-wrap"><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-video-controls-modal-rewind"></input><label for="cb-enh-video-controls-modal-rewind" class="ce-loc" data-ce-loc="set_rewind">Enable stream rewind</label><br></div><div><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-video-controls-modal-mirror-vid"></input><label for="cb-enh-video-controls-modal-mirror-vid" class="cursor-pointer ce-loc" data-ce-loc="mirror_video">Mirror video</label><br></div><div><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-video-controls-modal-invert-vid"></input><label for="cb-enh-video-controls-modal-invert-vid" class="cursor-pointer ce-loc" data-ce-loc="invert_video">Invert video</label></div><div><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-video-controls-modal-rotate-left"></input><label for="cb-enh-video-controls-modal-rotate-left" class="cursor-pointer ce-loc" data-ce-loc="rotate_left">Rotate left</label></div><div><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-video-controls-modal-rotate-right"></input><label for="cb-enh-video-controls-modal-rotate-right" class="cursor-pointer ce-loc" data-ce-loc="rotate_right">Rotate right</label></div><div><input type="range" id="cb-enh-video-controls-modal-brightness" data-default="100" min="0" max="200" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-brightness" class="ce-loc" data-ce-loc="brightness">Brightness</label></div><div><input type="range" id="cb-enh-video-controls-modal-contrast" data-default="100" min="0" max="200" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-contrast" class="ce-loc" data-ce-loc="contrast">Contrast</label></div><div><input type="range" id="cb-enh-video-controls-modal-saturation" data-default="100" min="0" max="200" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-saturation" class="ce-loc" data-ce-loc="saturation">Saturation</label></div><div><input type="range" id="cb-enh-video-controls-modal-sepia" data-default="0" value="0" min="0" max="100" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-sepia" class="ce-loc" data-ce-loc="sepia">Sepia</label></div><div><input type="range" id="cb-enh-video-controls-modal-hue" data-default="0" value="0" min="0" max="360" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-hue" class="ce-loc" data-ce-loc="hue">Hue</label></div><div><input type="range" id="cb-enh-video-controls-modal-blur" data-default="0" value="0" min="0" max="100" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-blur" class="ce-loc" data-ce-loc="blur">Blur</label></div><div><input type="range" id="cb-enh-video-controls-modal-volume-boost" data-default="1" value="1" min="1" max="6" step="0.1" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-volume-boost" class="ce-loc" data-ce-loc="volume_boost">Volume boost</label></div><div><input type="range" id="cb-enh-video-controls-modal-volume-voice-boost" data-default="1" value="1" min="1" max="24" step="1" class="cb-enh-vid-control-slider cb-enh-slider"><label for="volume" for="cb-enh-video-controls-modal-volume-voice-boost" class="ce-loc" data-ce-loc="volume_voice_boost">Voice volume boost</label></div><div><span class="ce-loc" data-ce-loc="zoom">Zoom</span>: <span id="cb-enh-video-controls-zoom-key"></span> + <span class="ce-loc" data-ce-loc="scroll">scroll</span> or <span class="ce-loc" data-ce-loc="left_click">left-click</span> + <span class="ce-loc" data-ce-loc="scroll">scroll</span>. <span class="ce-loc" data-ce-loc="zoom_dblc_reset">Double-click to reset</span>.</div><div class="cb-enh-video-controls-modal-btns"><input type="button" id="cb-enh-video-controls-modal-reset" value="Reset" class="cb-enh-btn ce-loc" data-ce-loc="reset"></div>`; const HTML_MULTI_VIEW_SETTINGS = `<label><input type="checkbox" class="cb-enh-checkbox" data-id="cb-enh-multi-btn-auto-hide"><span class="ce-loc" data-ce-loc="multi_auto_hide_offline">Hide offline cams</span></label><br><label><input type="checkbox" class="cb-enh-checkbox" data-id="cb-enh-multi-btn-auto-hide-priv"><span class="ce-loc" data-ce-loc="multi_auto_hide_priv">Hide private etc. cams</span></label><br><label><input type="checkbox" class="cb-enh-checkbox" data-id="cb-enh-multi-btn-auto-remove"><span class="ce-loc" data-ce-loc="multi_auto_remove_offline">Automatically remove offline cams</span></label><br><label><input type="checkbox" class="cb-enh-checkbox" data-id="cb-enh-multi-show-subject"><span class="ce-loc" data-ce-loc="multi_show_subject">Show cam subjects</span></label><br><label><input type="checkbox" class="cb-enh-checkbox" data-id="cb-enh-multi-unmute-hover"><span class="ce-loc" data-ce-loc="multi_hover_unmute">Play audio while hovering</span></label><br><label><input type="checkbox" class="cb-enh-checkbox" data-id="cb-enh-multi-align-right"><span class="ce-loc" data-ce-loc="multi_align_right">Align cams to the right</span></label><br><span class="ce-loc" data-ce-loc="max_quality">Max quality</span>: <select class="cb-enh-multi-max-quality cb-enh-input-select"><option value="2160">2160p</option><option value="1440">1440p</option><option value="1080">1080p</option><option value="720">720p</option><option value="480">480p</option></select><br><span class="ce-loc" data-ce-loc="multi_random_genders">Random cam genders</span>: <label><input type="checkbox" checked class="cb-enh-checkbox cb-enh-multi-checkbox-random-gender" data-gender="f"><span class="ce-loc" data-ce-loc="women">Women</span></label> <label><input type="checkbox" checked class="cb-enh-checkbox cb-enh-multi-checkbox-random-gender" data-gender="m"><span class="ce-loc" data-ce-loc="men">Men</span></label> <label><input type="checkbox" checked class="cb-enh-checkbox cb-enh-multi-checkbox-random-gender" data-gender="c"><span class="ce-loc" data-ce-loc="couples">Couples</span></label> <label><input type="checkbox" checked class="cb-enh-checkbox cb-enh-multi-checkbox-random-gender" data-gender="t"><span class="ce-loc" data-ce-loc="trans">Trans</span></label>`; const HTML_SETTINGS_CONTENT = `<div class="cb-enh-big-window-inner"><div class="cb-enh-big-window-header">Chaturbate Enhancer <span class="ce-loc" data-ce-loc="settings">Settings</span></div><div class="cb-enh-big-window-close-icon">` + SVG_CLOSE_ICON + `</div><div id="cb-enh-settings-group-selector-group-selector"><div data-group="general" class="cb-enh-settings-group-selector-group cb-enh-active ce-loc" data-ce-loc="set_group_general">General</div><div data-group="hidden" class="cb-enh-settings-group-selector-group ce-loc" data-ce-loc="hidden_cams">Hidden Cams</div><div data-group="hidden_countries" class="cb-enh-settings-group-selector-group ce-loc" data-ce-loc="hidden_countries">Hidden Countries</div><div data-group="multi" class="cb-enh-settings-group-selector-group ce-loc" data-ce-loc="multicam_viewer">Multi Cam Viewer</div><div data-group="export" class="cb-enh-settings-group-selector-group ce-loc" data-ce-loc="set_group_export_import">Export & Import</div><div data-group="info" class="cb-enh-settings-group-selector-group ce-loc" data-ce-loc="set_group_info_support">Info & Support</div></div><div class="cb-enh-big-window-content"><div class="cb-enh-settings-group cb-enh-active" data-group="general"><input type="checkbox" checked class="cb-enh-checkbox" id="cb-enh-settings-preview-rooms"></input><label for="cb-enh-settings-preview-rooms" class="ce-loc" data-ce-loc="preview_rooms">Preview cams on hover</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-preview-rooms-video"></input><label for="cb-enh-settings-preview-rooms-video" class="ce-loc" data-ce-loc="preview_rooms_video">Preview cams on hover as video</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-mute-big-previews"></input><label for="cb-enh-settings-mute-big-previews" class="ce-loc" data-ce-loc="mute_big_previews">Mute large cam previews</label><br><input type="checkbox" checked class="cb-enh-checkbox" id="cb-enh-settings-input-show-logo"></input><label for="cb-enh-settings-input-show-logo" class="ce-loc" data-ce-loc="set_show_site_logo">Show site logo in video player</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-small-margins"></input><label for="cb-enh-settings-small-margins" class="ce-loc" data-ce-loc="small_page_margins">Small page margins</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-blur-mute"></input><label for="cb-enh-settings-blur-mute" class="ce-loc" data-ce-loc="set_blur_mute">Mute inactive tabs</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-duration"></input><label for="cb-enh-settings-duration" class="ce-loc" data-ce-loc="set_duration">Show stream duration</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-inactive-load"></input><label for="cb-enh-settings-inactive-load" class="ce-loc" data-ce-loc="set_inactive_load">Load streams for inactive tabs</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-inactive-quality"></input><label for="cb-enh-settings-inactive-quality" class="ce-loc" data-ce-loc="set_inactive_quality">Reduce stream quality in inactive tabs</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-error-quality"></input><label for="cb-enh-settings-error-quality" class="ce-loc" data-ce-loc="set_error_quality">Reduce stream quality on errors</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-open-new-tab"></input><label for="cb-enh-settings-open-new-tab" class="ce-loc" data-ce-loc="set_open_new_tab">Open cams in new tabs</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-no-socials"></input><label for="cb-enh-settings-no-socials" class="ce-loc" data-ce-loc="set_no_socials">Hide social media links</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-no-merch-links"></input><label for="cb-enh-settings-no-merch-links" class="ce-loc" data-ce-loc="set_no_merch">Hide merch links</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-no-surveys"></input><label for="cb-enh-settings-no-surveys" class="ce-loc" data-ce-loc="set_no_surveys">Hide surveys</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-auto-rules"></input><label for="cb-enh-settings-auto-rules" class="ce-loc" data-ce-loc="set_auto_rules">Chat: Auto-accept rules</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-chat-hide-notices"></input><label for="cb-enh-settings-chat-hide-notices" class="ce-loc" data-ce-loc="chat_hide_notices">Chat: Hide notices</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-chat-hide-subject-notices"></input><label for="cb-enh-settings-chat-hide-subject-notices" class="ce-loc" data-ce-loc="chat_hide_subject_notices">Chat: Hide subject change messages</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-chat-hide-tips"></input><label for="cb-enh-settings-chat-hide-tips" class="ce-loc" data-ce-loc="chat_hide_tips">Chat: Hide tips</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-chat-hide-greys"></input><label for="cb-enh-settings-chat-hide-greys" class="ce-loc" data-ce-loc="chat_hide_greys">Chat: Hide messages from grey users</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-rec-exit-warn"></input><label for="cb-enh-settings-rec-exit-warn" class="ce-loc" data-ce-loc="rec_exit_warn">Recording: Warn before leaving while recording</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-rec-no-audio"></input><label for="cb-enh-settings-rec-no-audio" class="ce-loc" data-ce-loc="rec_wo_audio">Recording: Record without audio</label><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-rec-no-video"></input><label for="cb-enh-settings-rec-no-video" class="ce-loc" data-ce-loc="rec_wo_video">Recording: Record audio only</label><br><span class="ce-loc" data-ce-loc="time_format">Time format</span>: <select id="cb-enh-multi-settings-time-format" class="cb-enh-input-select"><option value="0">auto</option><option value="1">12h</option><option value="2">24h</option></select><br><input type="checkbox" class="cb-enh-checkbox" id="cb-enh-settings-rewind"></input><label for="cb-enh-settings-rewind" class="ce-loc" data-ce-loc="set_rewind">Enable stream rewind</label><br><span class="ce-loc" data-ce-loc="rewind_time">Rewind time</span>: <select id="cb-enh-multi-settings-rewind-time" class="cb-enh-input-select"><option value="0" class="ce-loc" data-ce-loc="set_rt_2">2 minutes</option><option value="1" class="ce-loc" data-ce-loc="set_rt_5">5 minutes</option><option value="2" class="ce-loc" data-ce-loc="set_rt_10">10 minutes</option></select><br><span class="ce-loc" data-ce-loc="set_show_these_genders">Show these genders in featured tab</span>: <input type="checkbox" checked class="cb-enh-checkbox cb-enh-settings-featuredg" id="cb-enh-settings-featuredg-f" data-gender="f"></input><label for="cb-enh-settings-featuredg-f" class="ce-loc" data-ce-loc="women">Women</label> <input type="checkbox" checked class="cb-enh-checkbox cb-enh-settings-featuredg" id="cb-enh-settings-featuredg-m" data-gender="m"></input><label for="cb-enh-settings-featuredg-m" class="ce-loc" data-ce-loc="men">Men</label> <input type="checkbox" checked class="cb-enh-checkbox cb-enh-settings-featuredg" id="cb-enh-settings-featuredg-c" data-gender="c"></input><label for="cb-enh-settings-featuredg-c" class="ce-loc" data-ce-loc="couples">Couples</label> <input type="checkbox" checked class="cb-enh-checkbox cb-enh-settings-featuredg" id="cb-enh-settings-featuredg-t" data-gender="t"></input><label for="cb-enh-settings-featuredg-t" class="ce-loc" data-ce-loc="trans">Trans</label><div style="margin-top:4px"></div><input type="button" id="cb-enh-settings-btn-recommended" value="Apply Recommended Settings" class="cb-enh-btn ce-loc" data-ce-loc="set_btn_recommended"><input type="button" id="cb-enh-settings-btn-mute-other-tabs" value="Mute Other Tabs" class="cb-enh-btn ce-loc" data-ce-loc="mute_other_tabs"></div><div class="cb-enh-settings-group" data-group="hidden"><div class="ce-loc" data-ce-loc="set_blocklist_info">Hidden cams doesn't appear on the site. You can hide cams using dot menu on cam list pages or button below streams.</div><div id="cb-enh-settings-blocklist-info"></div><div id="cb-enh-settings-blocklist-empty" class="ce-loc" data-ce-loc="set_blocklist_info_empty">You currently don't hide any cams.</div><div id="cb-enh-settings-blocklist-list"></div></div><div class="cb-enh-settings-group" data-group="hidden_countries"><div class="ce-loc" data-ce-loc="set_blocklist_cc_info">Hidden countries doesn't appear on the site. You can hide country using dot menu on cam list pages.</div><div id="cb-enh-settings-blocklist-cc-info"></div><div id="cb-enh-settings-blocklist-cc-empty" class="ce-loc" data-ce-loc="set_blocklist_cc_info_empty">You currently don't hide any countries.</div><div id="cb-enh-settings-blocklist-cc-list"></div></div><div class="cb-enh-settings-group" data-group="multi">` + HTML_MULTI_VIEW_SETTINGS + `</div><div class="cb-enh-settings-group" data-group="export"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-settings-btn-export" value="Export settings to file" data-ce-loc="set_btn_export"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-settings-btn-import" value="Import settings from file" data-ce-loc="set_btn_import"><input type="file" accept="application/json" class="cb-enh-btn ce-loc" id="cb-enh-settings-btn-import-file" style="display:none"><div id="cb-enh-settings-export-info"></div></div><div class="cb-enh-settings-group" data-group="info"></div><div id="cb-enh-settings-footer">Chaturbate™ Enhancer v` + gVersion + ` © <a href="https://improper.dev/" target="_blank" rel="noopener" referrerpolicy="origin">improper.dev</a></div></div>`; const gRecordWarnModalHTML = `<div class="cb-enh-big-window-inner"><div class="cb-enh-big-window-header"><span class="ce-loc" data-ce-loc="notice">Notice</span></div><div class="cb-enh-big-window-close-icon">{{SVG_CLOSE_ICON}}</div><div class="cb-enh-big-window-content"><span class="ce-loc" data-ce-loc="warn_rec_inprogress">Recording is in progress. Stop recording first.</span></div></div>`; const acre6 = 'a1lOjBM8aQ0duanUhryu'; const settingsContentExtraHTML = `<div id="` + gtrrft('blso') + `" class="cb-enh-hidden"><div class="`+gtrrft('strpb')+`" id="` + gtrrft('bls') + `"></div></div>`; const settingsContentBlocklistUsernameHTML = `<div class="cb-enh-st-blocklist-username"><div class="cb-enh-st-blocklist-remove-icon cb-enh-st-blocklist-username-remove-icon" data-username="{{username}}">` + SVG_CLOSE_ICON + `</div><span>{{username}}</span></div>`; const settingsContentBlocklistCountryHTML = `<div class="cb-enh-st-blocklist-country"><div class="cb-enh-st-blocklist-remove-icon cb-enh-st-blocklist-country-remove-icon" data-country="{{country_code}}">` + SVG_CLOSE_ICON + `</div><span>{{country_name}}</span></div>`; const gBlocklistRooms = []; function blocklistLoad() { if(gIsFullVideoPage || !localStorage) { return; } const ldata = blocklistLoadData(); ldata['list'].forEach((username) => { if(!gBlocklistRooms.includes(username)) { blocklistAdd(username); } }); [...gBlocklistRooms].forEach((username) => { if(!ldata['list'].includes(username)) { blocklistRemove(username); } }); } function blocklistLoadData() { if(!localStorage) { return { 'list': [] } } let ldata = localStorage.getItem('cb-enh-blocklist'); if(ldata === null) { return { 'list': [] } } try { ldata = JSON.parse(ldata); } catch(e) { return { 'list': [] } } if(!('list' in ldata)) { return { 'list': [] } } return ldata; } const gBlocklistStyles = []; function blocklistAddStyle(username) { if(username in gBlocklistStyles) { return; } const blocklistStyle = `.RoomCard:has(a[data-room="` + username + `"]), .roomCard:has(a[data-room="` + username + `"]) {display: none !important;}`; gBlocklistStyles[username] = addStyle(blocklistStyle); } function blocklistRemoveStyle(username) { if(!(username in gBlocklistStyles)) { return; } gBlocklistStyles[username].remove(); delete gBlocklistStyles[username]; } blocklistLoad(); function blocklistAdd(username) { if(gBlocklistRooms.includes(username)) { return; } blocklistAddStyle(username); gBlocklistRooms.push(username); } function blocklistRemove(username) { if(!gBlocklistRooms.includes(username)) { return; } const index = gBlocklistRooms.indexOf(username); if(index > -1) { gBlocklistRooms.splice(index, 1); } blocklistRemoveStyle(username); } function blocklistSave() { if(!localStorage) { return; } const ldata = { 'list': gBlocklistRooms }; localStorage.setItem('cb-enh-blocklist', JSON.stringify(ldata)); const isListEmpty = ldata['list'].length === 0; queryToggleElement('#cb-enh-settings-blocklist-empty', isListEmpty); queryToggleElement('#cb-enh-settings-blocklist-info', !isListEmpty); } function blocklistClear() { for(const v of [...gBlocklistRooms]) { blocklistRemove(v); } } function blocklistCountryAddStyle(country) { if(country in gBlocklistCountryStyles) { return; } const blocklistStyle = `.RoomCard:not(:has(.RoomCard__followStar.zGQx7GmMy4m1dlBUX9Rw)):has(.thumbnail_flag > span.fi-` + country + `) {display: none !important;}`; gBlocklistCountryStyles[country] = addStyle(blocklistStyle); } function blocklistCountryRemoveStyle(country) { if(!(country in gBlocklistCountryStyles)) { return; } gBlocklistCountryStyles[country].remove(); delete gBlocklistCountryStyles[country]; } function blocklistCountryAdd(country) { if(!VALID_COUNTRIES.includes(country)) { return; } if(gBlocklistCountries.includes(country)) { return; } blocklistCountryAddStyle(country); gBlocklistCountries.push(country); } function blocklistCountryRemove(country) { if(!gBlocklistCountries.includes(country)) { return; } const index = gBlocklistCountries.indexOf(country); if(index > -1) { gBlocklistCountries.splice(index, 1); } blocklistCountryRemoveStyle(country); } function blocklistCountrySave() { if(!localStorage) { return; } setSetting('blocklist-cc', gBlocklistCountries); const isListEmpty = gBlocklistCountries.length === 0; queryToggleElement('#cb-enh-settings-blocklist-cc-empty', isListEmpty); queryToggleElement('#cb-enh-settings-blocklist-cc-info', !isListEmpty); } function blocklistCountryClear() { for(const v of [...gBlocklistCountries]) { blocklistCountryRemove(v); } } function blocklistCountryLoadData() { if(!localStorage) { return []; } const list = getSetting('blocklist-cc'); if(!list) { return []; } return list; } function blocklistCountryLoad() { if(gIsFullVideoPage || !localStorage) { return; } const list = blocklistCountryLoadData(); list.forEach((country) => { if(!gBlocklistCountries.includes(country)) { blocklistCountryAdd(country); } }); [...gBlocklistCountries].forEach((country) => { if(!list.includes(country)) { blocklistCountryRemove(country); } }); } function setChatNoticesHidden(hidden) { document.body.classList.toggle('cb-enh-chat-hide-notices', hidden === true); } function setChatSubjectNoticesHidden(hidden) { document.body.classList.toggle('cb-enh-chat-hide-subject-notices', hidden === true); } function setChatTipsHidden(hidden) { document.body.classList.toggle('cb-enh-chat-hide-tips', hidden === true); } function setChatGreysHidden(hidden) { document.body.classList.toggle('cb-enh-chat-hide-greys', hidden === true); } function setAutoChatRules(enable) { document.body.classList.toggle('cb-enh-arl', enable); } function setStreamDurationEnabled(enable) { document.body.classList.toggle('cb-enh-drt', enable); } function setNoSocials(enable) { document.body.classList.toggle('cb-enh-nsc', enable); updateBioRows(); } function setNoMerchLinks(enable) { document.body.classList.toggle('cb-enh-nml', enable); } function setNoSurveys(enable) { document.body.classList.toggle('cb-enh-nss', enable); } const gMoreMenuHTML = `<ul id="cb-enh-room-more-menu" class="cb-enh-room-more-menu"><li id="cb-enh-more-menu-hide" class="ce-loc" data-ce-loc="hide_room">Hide cam</a><li id="cb-enh-more-menu-hide-cc"><span class="ce-loc" data-ce-loc="hide_country">Hide country</span> (<span class="cb-enh-more-menu-country-name"></span>)</a></ul>`; function setRoomMoreMenuVisible(visible, x, y, country = false) { let elMenu = document.getElementById('cb-enh-room-more-menu'); if(!visible) { hideElement(elMenu); } else { queryToggleElement('#cb-enh-more-menu-hide-cc', country !== null); elMenu.style.width = country !== null ? '180px' : '120px'; elMenu.style.height = country !== null ? '52px' : '26px'; if(country) { elMenu.querySelector('.cb-enh-more-menu-country-name').innerText = getLocale('cc_' + country); } elMenu.style.left = x + 'px'; elMenu.style.top = y + 'px'; showElement(elMenu); } gRoomMoreMenuVisible = visible; } let regRedirPreventClick = false; const navSelector = 'nav[data-testid="header-nav-bar"]'; function addHeaderLinks(eNav) { eNav.insertAdjacentHTML('beforeend', multiLinkHTML); if(isMultiViewPage()) { document.querySelector('a.HeaderNavBar__link[href="/multicam/"]')?.classList.add('HeaderNavBar__link--active'); multiInitGridSizeSelector(eNav); } } if(!gIsFullVideoPage) { const observer = new MutationObserver(() => { const el = document.querySelector(navSelector); if(el) { observer.disconnect(); addHeaderLinks(el); } }); observer.observe(document.documentElement || document, { childList: true, subtree: true }); } function hasUserInteracted() { return !!(navigator.userActivation?.hasBeenActive || navigator.userActivation?.isActive); } function removeWindowInteractionEvents() { window.removeEventListener('click', onFirstWindowInteraction); window.removeEventListener('keydown', onFirstWindowInteraction); window.removeEventListener('touchstart', onFirstWindowInteraction); window.removeEventListener('pointerdown', onFirstWindowInteraction); } function onFirstWindowInteraction(e) { if(!e.isTrusted) { return; } if(!hasUserInteracted()) { return; } removeWindowInteractionEvents(); maybeUnmuteCurrentVideo(); } if(!hasUserInteracted() && !gIsFullVideoPage) { window.addEventListener('click', onFirstWindowInteraction); window.addEventListener('keydown', onFirstWindowInteraction); window.addEventListener('touchstart', onFirstWindowInteraction); window.addEventListener('pointerdown', onFirstWindowInteraction); setTimeout(() => { if(hasUserInteracted()) { removeWindowInteractionEvents(); } }, 100); } function waitForElement(selector, callback) { const observer = new MutationObserver((mutations, obs) => { const el = document.querySelector(selector); if(el) { obs.disconnect(); callback(el); } }); observer.observe(document.documentElement || document, { childList: true, subtree: true }); }; document.addEventListener('DOMContentLoaded', function() { if(gIsFullVideoPage) { return; } initGenderNavigation(); if(!gIsNavigatingToRandom && isRandomPage()) { goToRandomRoom(false, unsafeWindow.__cnh__1 && unsafeWindow.__cnh__1.getVisibilityState() === 'visible', window.location.pathname); } else { const navTo = localStorage.getItem('cb-enh-n'); if(navTo && window.location.pathname === '/') { const params = new URLSearchParams(window.location.search); if(params && params.get('i') === '') { gHbx = navTo; localStorage.removeItem('cb-enh-n'); disableDynamicLinks(); addStyle(`.followButton, .fanclubButton, .sendTipButton, .sendDMButton, .promoteRoomLink, a[data-testid="cam-to-cam-button"], .reportAbuseLink, div[data-paction="CurrentShowBuyBox"] .currentBalance, #ChatTabContainer, #AppPanelTable, .photoVideoDetailSection .title a:has(.popoutIcon), .theater-overlay #user-list, .theater-overlay #chat-btn, .theater-overlay #send-tip, .theater-overlay div[data-testid="private-show-btn"], .theater-overlay div[data-testid="cam-to-cam-button"] {display: none !important;}div[data-testid="unlock-button"] {cursor: not-allowed !important;}`); document.addEventListener('click', e => { const el = e.target.closest('div[data-testid="unlock-button"]'); if(!el) { return; } e.preventDefault(); e.stopImmediatePropagation(); }, true); navigateDynamic('/' + navTo); } } } const elOverlay = document.createElement('div'); elOverlay.id = 'cb-enh-page-overlay'; document.body.append(elOverlay); document.body.insertAdjacentHTML('beforeend', '<div id="cb-enh-schedule-hour-tooltip" class="cb-enh-schedule-tooltip-wrap"><div class="cb-enh-schedule-tooltip-ar"></div><div class="cb-enh-schedule-tooltip-inner"></div></div>'); gElScheduleTooltip = document.getElementById('cb-enh-schedule-hour-tooltip'); gElScheduleTooltipInner = gElScheduleTooltip.lastElementChild; gLschedChance = getLocale('sched_chance'); if(hasUserInteracted()) { removeWindowInteractionEvents(); } setTimeout(() => { if(hasUserInteracted()) { removeWindowInteractionEvents(); } }, 100); enableDarkMode(); if(getSetting('reg-redir') === 2) { setSetting('reg-redir', null); setSetting('reg-redir-room', null); document.querySelector('.logo-zone a')?.click(); regRedirPreventClick = true; } if(isMultiViewPage()) { initMultiView(); } else if(window.location.pathname.startsWith('/roomlogin/')) { } else if('initialRoomDossier' in unsafeWindow && !window.location.pathname.startsWith('/p/') && !window.location.pathname.startsWith('/b/')) { if(unsafeWindow.initialRoomDossier === '') { gCurrentBroadcaster = null; gCurrentBroadcasterGender = null; } else { let userData = JSON.parse(unsafeWindow.initialRoomDossier); gCurrentBroadcaster = userData.broadcaster_username; gCurrentBroadcasterGender = userData.broadcaster_gender; updateRandomLink(); } enhanceRoom(); } if(!gCurrentBroadcaster) { gCurrentBroadcaster = getCurrentBroadcasterName(); } getHiddenGenders().forEach(function(v) { document.body.classList.toggle('cb-enh-gb-' + v, true); }); initAnimatedThumbs(); initMultiViewUINavigation(); initVideoControlsModal(); initVideoControlAnimationHints(); initBelowVideoButtonsEvents(); initSupportInfoEvents(); initGridSizeSelector(); initMoreGridSizeSelector(); const settings = document.createElement('div'); settings.className = 'cb-enh-big-window'; settings.id = 'cb-enh-settings-window'; settings.innerHTML = HTML_SETTINGS_CONTENT; document.body.append(settings); document.body.insertAdjacentHTML('beforeend', settingsContentExtraHTML); document.addEventListener('click', (e) => { let el = e.target.closest('.cb-enh-vcontrols-btn-vol'); if(el) { if(gIsInMultiView) { const elBlock = el.closest('.cb-enh-multi-cam'); if(!gMultiIframeMode) { const elVideo = elBlock.querySelector('video'); elVideo.muted = !elVideo.muted; } else { let muted = false; let volume = 1; if(elBlock._data && elBlock._data.volume) { muted = !elBlock._data.volume.muted; volume = elBlock._data.volume.volume; } multiSetIframeVolume(elBlock, muted, volume); } } else { const elVideoHolder = el.closest('.cb-enh-rewinded'); const elVideo = elVideoHolder ? getVideoHolderRewindVideo(elVideoHolder) : null; if(elVideo) { elVideo.muted = !elVideo.muted; } updateVideoUI(elVideoHolder); } setVideoControlsVisible(false); e.preventDefault(); e.stopImmediatePropagation(); return; } el = e.target.closest('.cb-enh-settings-group-selector-group'); if(el) { const group = el.dataset.group; queryAndRun('.cb-enh-settings-group.cb-enh-active', el => { hideElement(el); el.classList.remove('cb-enh-active'); }); queryAndRun('.cb-enh-settings-group[data-group="' + group + '"]', el => { showElement(el); el.classList.add('cb-enh-active'); }); queryAndRun('.cb-enh-settings-group-selector-group.cb-enh-active', el => { el.classList.remove('cb-enh-active'); }); el.classList.add('cb-enh-active'); document.getElementById('cb-enh-settings-export-info').innerText = ''; return; } el = e.target.closest('#cb-enh-settings-btn-export'); if(el) { settingsExport(); return; } el = e.target.closest('#cb-enh-settings-btn-import'); if(el) { document.getElementById('cb-enh-settings-btn-import-file').click(); return; } el = e.target.closest('#cb-enh-settings-btn-mute-other-tabs'); if(el) { muteAllOtherTabs(); return; } el = e.target.closest('#cb-enh-settings-btn-recommended'); if(el) { settingsApplyRecommended(); return; } el = e.target.closest('a.GenderNav__button.cb-enh-gender-nav-btn'); if(el) { e.preventDefault(); e.stopImmediatePropagation(); if(window.location.pathname.startsWith('/discover/')) { queryAndRun('button.GenderNav__button[data-testid="' + el.getAttribute('data-testid') + '"]', el => { el.click(); }); } else { navigateDynamic(el.href); } queryAndRun('a.GenderNav__button.active', el => { el.classList.remove('active'); }); el.classList.add('active'); return; } el = e.target.closest('a.HeaderHomeLink, a#home'); if(el) { if(canBeDynamicallyNavigatedFrom()) { e.preventDefault(); e.stopImmediatePropagation(); navigateDynamic(el.href); } return; } el = e.target.closest('a#following.HeaderNavBar__link'); if(el) { runNextTick(function() { updateFollowedDropdownUI(); if(isFollowedDropdownOpen()) { gIntvFollowedDropDownUIUpdate = setInterval(function() { updateFollowedDropdownUI(); }, 1000); } else { gIntvFollowedDropDownUIUpdate = clearIntervalEx(gIntvFollowedDropDownUIUpdate); } }); } }); document.addEventListener('pointerdown', (e) => { if(!e.target || typeof e.target.closest !== 'function') { return; } let el = e.target.closest('.cb-enh-vcontrols-btn.slider'); if(el) { el.setPointerCapture(e.pointerId); updateSliderPosition(e, el); return; } }); document.addEventListener('pointermove', (e) => { if(!e.target || typeof e.target.closest !== 'function') { return; } let el = e.target.closest('.cb-enh-vcontrols-btn.slider'); if(el && el.hasPointerCapture(e.pointerId)) { updateSliderPosition(e, el); } }); function onPointerDownUp(e) { if(!e.target || typeof e.target.closest !== 'function') { return; } const el = e.target.closest('.cb-enh-click-select'); if(!el) { return; } const range = document.createRange(); range.selectNodeContents(el); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } document.addEventListener('pointerdown', onPointerDownUp); document.addEventListener('pointerup', onPointerDownUp); document.addEventListener('mousedown', (e) => { const el = e.target.closest('.cb-enh-big-window-content label') || e.target.closest('#cb-enh-video-controls-modal-content label') || e.target.closest('.cb-enh-multi-info label'); if(!el) { return; } if(e.detail === 0) { return; } e.preventDefault(); window.getSelection().removeAllRanges(); }); document.addEventListener('click', (e) => { let el = e.target.closest('#cb-enh-more-menu-hide'); if(el) { const username = document.querySelector('#cb-enh-room-more-menu').dataset.username; blocklistAdd(username); blocklistSave(); return; } el = e.target.closest('#cb-enh-more-menu-hide-cc'); if(el) { const country = document.querySelector('#cb-enh-room-more-menu').dataset.country; blocklistCountryAdd(country); blocklistCountrySave(); return; } }); document.addEventListener('change', (e) => { const el = e.target; if(el.matches('#cb-enh-settings-btn-import-file')) { settingsImportOnFileDrop(e); return; } if(el.matches('#cb-enh-multi-settings-time-format')) { setSetting('time-format', parseInt(el.value)); scheduleUpdateHourLabels(); return; } if(el.matches('#cb-enh-multi-settings-rewind-time')) { setSetting('rewind-time', parseInt(el.value)); rewindUpdateAll(); return; } if(!el.matches('input[type="checkbox"]')) { return; } const enable = el.checked; const id = el.id || el.dataset.id; switch(id) { case 'cb-enh-settings-preview-rooms': const elCheckboxComponent = document.querySelector('#animate_thumbnails_form .checkboxComponent'); if(elCheckboxComponent) { elCheckboxComponent.classList.toggle('checked', enable); } setSetting('animate_thumbnails', enable); if(!enable) { document.getElementById('cb-enh-settings-preview-rooms-video').checked = false; document.getElementById('cb-enh-settings-preview-rooms-video').dispatchEvent(new Event('change', { bubbles: true })); } break; case 'cb-enh-settings-preview-rooms-video': setSetting('disable-big-previews', !enable); gBigPreviewEnabled = enable; if(enable) { document.getElementById('cb-enh-settings-preview-rooms').checked = true; document.getElementById('cb-enh-settings-preview-rooms').dispatchEvent(new Event('change', { bubbles: true })); } break; case 'cb-enh-settings-input-show-logo': setSetting('hide-vid-logo', !enable); setVideoLogoVisible(enable); queryAndRun('#cb-enh-video-controls-modal-show-logo', (el) => { el.checked = enable; }); break; case 'cb-enh-settings-rewind': setSetting('rewind', enable); rewindSetEnabled(enable); queryAndRun('#cb-enh-video-controls-modal-rewind', (el) => { el.checked = enable; }); break; case 'cb-enh-settings-auto-rules': setSetting('auto-chat-rules', enable); setAutoChatRules(enable); break; case 'cb-enh-settings-blur-mute': setSetting('blur-mute', enable); break; case 'cb-enh-settings-duration': setSetting('duration', enable); setStreamDurationEnabled(enable); queryAndRun('#cb-enh-video-controls-modal-duration', (el) => { el.checked = enable; }); break; case 'cb-enh-settings-inactive-quality': setSetting('inactive-quality', enable); updateFakeVisibility(); break; case 'cb-enh-settings-error-quality': setSetting('error-quality', enable); if(unsafeWindow.__cnh__fnc__) { unsafeWindow.__cnh__fnc__.patchHLS(!enable); } break; case 'cb-enh-settings-inactive-load': setSetting('inactive-load', enable); updateFakeVisibility(); break; case 'cb-enh-settings-chat-hide-notices': setSetting('chat-hide-notices', enable); setChatNoticesHidden(enable); break; case 'cb-enh-settings-chat-hide-subject-notices': setSetting('chat-hide-subject-notices', enable); setChatSubjectNoticesHidden(enable); break; case 'cb-enh-settings-chat-hide-tips': setSetting('chat-hide-tips', enable); setChatTipsHidden(enable); break; case 'cb-enh-settings-chat-hide-greys': setSetting('chat-hide-greys', enable); setChatGreysHidden(enable); break; case 'cb-enh-settings-rec-exit-warn': setSetting('rec-exit-warn', enable); gRecordingExitWarn = enable; break; case 'cb-enh-settings-rec-no-audio': setSetting('rec-no-audio', enable); gRecordAudio = !enable; if(enable) { document.getElementById('cb-enh-settings-rec-no-video').checked = false; document.getElementById('cb-enh-settings-rec-no-video').dispatchEvent(new Event('change', { bubbles: true })); } break; case 'cb-enh-settings-rec-no-video': setSetting('rec-no-video', enable); gRecordVideo = !enable; if(enable) { document.getElementById('cb-enh-settings-rec-no-audio').checked = false; document.getElementById('cb-enh-settings-rec-no-audio').dispatchEvent(new Event('change', { bubbles: true })); } break; case 'cb-enh-settings-mute-big-previews': setSetting('mute-big-previews', enable); gBigPreviewPlayAudio = !enable; break; case 'cb-enh-settings-small-margins': setSetting('smargins', enable); setSmallPageMargins(enable); break; case 'cb-enh-video-controls-modal-mirror-vid': updateCurrentVideoTransformStyle(gVideoControlsCurrentVideoHolder, { 'mirrored': enable }); break; case 'cb-enh-video-controls-modal-invert-vid': updateCurrentVideoTransformStyle(gVideoControlsCurrentVideoHolder, { 'inverted': enable }); break; case 'cb-enh-video-controls-modal-rotate-left': updateCurrentVideoTransformStyle(gVideoControlsCurrentVideoHolder, { 'rotation': enable ? false : null }); if(enable) { document.getElementById('cb-enh-video-controls-modal-rotate-right').checked = !enable; } break; case 'cb-enh-video-controls-modal-rotate-right': updateCurrentVideoTransformStyle(gVideoControlsCurrentVideoHolder, { 'rotation': enable ? true : null }); if(enable) { document.getElementById('cb-enh-video-controls-modal-rotate-left').checked = !enable; } break; case 'cb-enh-video-controls-modal-show-logo': setSetting('hide-vid-logo', !enable); setVideoLogoVisible(enable); document.getElementById('cb-enh-settings-input-show-logo').checked = enable; break; case 'cb-enh-video-controls-modal-rewind': setSetting('rewind', enable); rewindSetEnabled(enable); queryAndRun('#cb-enh-settings-rewind', (el) => { el.checked = enable; }); break; case 'cb-enh-video-controls-modal-duration': setSetting('duration', enable); setStreamDurationEnabled(enable); document.getElementById('cb-enh-settings-duration').checked = enable; break; case 'cb-enh-multi-btn-auto-remove': setSetting('multi-auto-remove', enable); if(enable) { multiRemoveOfflineRooms(); } break; case 'cb-enh-multi-btn-auto-hide': setSetting('multi-auto-hide', enable); multiHidePrivateRoomsIfNeeded(); break; case 'cb-enh-multi-btn-auto-hide-priv': setSetting('multi-auto-hide-priv', enable); multiHidePrivateRoomsIfNeeded(); break; case 'cb-enh-multi-show-subject': setSetting('multi-show-subject', enable); document.body.classList.toggle('cb-enh-multi-show-subject', enable); break; case 'cb-enh-multi-unmute-hover': setSetting('multi-unmute-hover', enable); gMultiUnmuteOnHover = enable; if(enable) { multiMuteAllVideos(); } break; case 'cb-enh-multi-align-right': setSetting('multi-align-right', enable); multiSetAlignRight(enable); break; case 'cb-enh-settings-open-new-tab': setSetting('open-new-tab', enable); gOpenRoomsInNewTab = enable; break; case 'cb-enh-settings-no-socials': setSetting('no-socials', enable); setNoSocials(enable); break; case 'cb-enh-settings-no-merch-links': setSetting('no-merch-links', enable); setNoMerchLinks(enable); break; case 'cb-enh-settings-no-surveys': setSetting('no-surveys', enable); setNoSurveys(enable); break; } if(id && id.startsWith('cb-enh-multi-')) { multiUpdateSettings(); } }); document.addEventListener('change', (e) => { const el = e.target.closest('.cb-enh-settings-featuredg'); if(!el) { return; } if(!localStorage) { return; } const gender = el.getAttribute('data-gender'); const show = el.checked; const hiddenGenders = getHiddenGenders(); const pos = hiddenGenders.indexOf(gender); if(pos !== -1) { hiddenGenders.splice(pos, 1); } if(!show) { hiddenGenders.push(gender); } document.body.classList.toggle('cb-enh-gb-' + gender, !show); setSetting('featured-hidden-genders', hiddenGenders); }); document.addEventListener('change', (e) => { const el = e.target.closest('.cb-enh-multi-checkbox-random-gender'); if(!el) { return; } if(!localStorage) { return; } const gender = el.getAttribute('data-gender'); const enabled = el.checked; const genders = multiGetRandomGenders(); const pos = genders.indexOf(gender); if(pos !== -1) { genders.splice(pos, 1); } if(enabled) { genders.push(gender); } setSetting('multi-random-genders', genders); if(genders.length === 0) { setSetting('multi-random-genders', multiGetRandomGenders()); } multiUpdateSettings(); }); document.addEventListener('click', (e) => { if(e.target.closest('#cb-enh-pip')) { setVideoControlsVisible(false); let elVideo; if(!isMultiViewPage()) { elVideo = getCurrentVideo(); } else { if(gMultiIframeMode) { const elIframe = e.target.closest('.cb-enh-multi-cam').querySelector('.cb-enh-multi-video-iframe'); sendIframeEvent(elIframe, 'toggle-pip'); return; } elVideo = e.target.closest('#cb-enh-pip').closest('.cb-enh-multi-cam-wrap2').querySelector('video'); } if(!elVideo) { return; } if(document.pictureInPictureElement === elVideo) { exitVideoPiP(); return; } if(document.fullscreenElement) { document.exitFullscreen(); } elVideo.requestPictureInPicture(); return; } if(e.target.closest('.cb-enh-row-value-fanclub')) { const elFanclubBtn = document.querySelector('.fanclubButton'); if(elFanclubBtn && !isElementHidden(elFanclubBtn)) { elFanclubBtn?.click(); } return; } if(e.target.closest('#cb-enh-random-cam')) { goToRandomRoom(gOpenRoomsInNewTab, true); e.preventDefault(); e.stopImmediatePropagation(); return; } }); document.addEventListener('click', (e) => { if(e.target.closest('#theater-mode-icon') || e.target.closest('#full-screen-icon')) { exitVideoPiP(); return; } }); document.addEventListener('mouseover', (e) => { const el = e.target.closest('.cb-enh-room-preview-icon'); if(!el) { return; } if(e.relatedTarget && el.contains(e.relatedTarget)) { return; } if(!gBigPreviewEnabled) { return; } if(el.parentElement.querySelector('.thumbnail_label_c_private_show') || el.parentElement.querySelector('.thumbnail_label_offline') || el.parentElement.querySelector('.thumbnail_label_c_away')) { return; } setRoomMoreMenuVisible(false); const roomName = el.parentElement.querySelector('a[data-room]').getAttribute('data-room'); let elPreviewImg = null; if(!gBigPreviewVideo || !isVideoPlaying(gBigPreviewVideo)) { const elThumb = el.parentElement.querySelector('img.RoomCardThumbnail__image, img.room_thumbnail, img.FollowedDropdown__room-image'); elPreviewImg = elThumb.cloneNode(true); } gBigPreviewRoomCard = el.parentElement; bigPreviewShow(roomName, elPreviewImg, el.parentElement); }); document.addEventListener('mouseout', (e) => { const el = e.target.closest('.cb-enh-room-preview-icon'); if(!el) { return; } if(e.relatedTarget && el.contains(e.relatedTarget)) { return; } bigPreviewHide(); }); document.addEventListener('scroll', function() { if(gBigPreviewShown) { bigPreviewHide(); } }); document.addEventListener('mouseover', (e) => { const el = e.target.closest('.RoomCard, .roomCard, .FollowedDropdown__room'); if(!el) { return; } if(e.relatedTarget && el.contains(e.relatedTarget)) { return; } if(!gBigPreviewEnabled) { return; } if(el.querySelector('.thumbnail_label_c_private_show') || el.querySelector('.thumbnail_label_offline') || el.querySelector('.thumbnail_label_c_away')) { return; } el.setAttribute('data-show-big-preview-icon', '1'); const roomName = (el.querySelector('a[data-room]')?.getAttribute('data-room')) || el.querySelector('img.FollowedDropdown__room-image')?.getAttribute('alt'); if(!roomName) { return; } let elPreviewImg = null; if(gBigPreviewPrestart) { const elThumb = el.querySelector('img.RoomCardThumbnail__image, img.room_thumbnail, img.FollowedDropdown__room-image'); if(!elThumb) { return; } elPreviewImg = elThumb.cloneNode(true); } gBigPreviewRoomCard = el; (gBigPreviewRoomCard.querySelector('a[data-room]') || gBigPreviewRoomCard.querySelector('a.FollowedDropdown__room-link')).prepend(gBigPreviewVideo); bigPreviewPreloadHLS(roomName, elPreviewImg); }); document.addEventListener('mouseout', (e) => { const el = e.target.closest('.RoomCard, .roomCard, .FollowedDropdown__room'); if(!el) { return; } if(e.relatedTarget && el.contains(e.relatedTarget)) { return; } gIntvBigPreviewPreloadHLS = clearTimeoutEx(gIntvBigPreviewPreloadHLS); if(gBigPreviewPrestart) { bigPreviewUnloadVideo(); } el.removeAttribute('data-show-big-preview-icon'); }); document.body.insertAdjacentHTML('beforeend', `<div id="cb-enh-room-preview"><div id="cb-enh-room-preview-preview"><img id="cb-enh-room-preview-img"><img id="cb-enh-room-preview-img2"><video id="cb-enh-room-preview-video" autoplay class="vjs-tech cb-enh-video" id="vjs_video_3_html5_api" tabindex="-1" role="application"></video></div><div id="cb-enh-room-preview-info"><div id="cb-enh-room-preview-info-top"><div id="cb-enh-room-preview-username"></div><div id="cb-enh-room-preview-info-right"><div id="cb-enh-room-preview-info-age" class="camSubjectColor"></div><div id="cb-enh-room-preview-info-gender"></div><div id="cb-enh-room-preview-info-flag"></div></div></div><div id="cb-enh-room-preview-subject" class="camSubjectColor"></div><div id="cb-enh-room-preview-location" class="camSubjectColor"></div><div id="cb-enh-room-preview-caminfo" class="camSubjectColor"></div></div></div>` ); gBigPreviewVideo = document.getElementById('cb-enh-room-preview-video'); gBigPreviewVideo.addEventListener('playing', () => { if(gBigPreviewShown || !gBigPreviewRoomCard) { return; } gBigPreviewRoomCard.querySelector('img.RoomCardThumbnail__image, img.room_thumbnail, img.FollowedDropdown__room-image').style.opacity = '0'; gBigPreviewRoomCard.querySelector('.cb-enh-room-preview-frame')?.remove(); }); document.addEventListener('click', (e) => { const el = e.target.closest('.cb-enh-room-preview-icon'); if(!el) { return; } e.preventDefault(); e.stopImmediatePropagation(); bigPreviewHide(); }); document.addEventListener('click', function(e) { const el = e.target.closest('.FollowedDropdown__room'); if(!el) { return; } requestAnimationFrame(() => { const followingLinkOpen = document.querySelector('a.HeaderNavBar__link--dropdown-shown'); if(followingLinkOpen) { followingLinkOpen.click(); } }); }, true); document.addEventListener('click', function(e) { const el = e.target.closest('.RoomCard, .roomCard') || e.target.closest('.FollowedDropdown__room') || e.target.closest('.room_thumbnail_container'); if(!el) { return; } if(e.target.closest('.RoomCard__followStar')) { return; } bigPreviewUnloadVideo(); }, true); const userType = getUserType(); const msg = getSupportMessage(userType); if(msg !== null) { document.addEventListener('click', (e) => { if(e.target.closest('#' + gtrrft('bls')) || e.target.closest('.cb-enh-settings-group[data-group="info"]')) { loutrreg(gCurrentBroadcaster); } }); document.querySelector('#' + gtrrft('bls')).innerHTML = msg; } if(userType === 2) { addStyle(`.`+gtrrft('sp')+` {display: none;}`); } applySettings(); if(gHideVideoLogoStyle) { gHideVideoLogoStyle.remove(); } document.body.insertAdjacentHTML('beforeend', gMoreMenuHTML); const cFunc = function() { const broadcasterName = getCurrentBroadcasterName(); if(!broadcasterName) { alert(getLocale('err_vurl_no_room')); return; } let furl = '/api/chatvideocontext/' + broadcasterName + '/'; if(gCurrentRoomIsInaccessible) { furl = 'https://cb-enh-api2.improper.dev/api/room/' + broadcasterName + '?key=' + acre7; } xmlhttpRequest({ method: 'GET', url: furl, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Referer': 'https://' + window.location.hostname + '/' + broadcasterName + '/', }, timeout: 60*1*1000, anonymous: gHbx !== null, onload: function(resp) { let data; try { data = JSON.parse(resp.responseText); } catch(e) { alert(getLocale('err_vurl_unk')); return; } if(!('hls_source' in data) || data['hls_source'] === '') { alert(getLocale('err_vurl')); return; } let srcurl = cutPlaylistUrl(data['hls_source']); if(!srcurl) { alert(getLocale('err_vurl')); return; } srcurl = cutPlaylistUrl(srcurl); const copied = setClipboard(srcurl, 'text'); let infoText = srcurl; if(copied) { infoText += '\n\n(' + getLocale('copied_to_clipboard') + ')'; } alert(infoText); } }); } if(!gIsAG) { registerMenuCommand(getLocale('get_vsurl'), cFunc, 'g'); } setTimeout(function() { bigPreviewCacheClearAboveLimit(); }, 1000); document.addEventListener('visibilitychange', () => { const visibilityState = unsafeWindow.__cnh__1.getVisibilityState(); if(!visibilityState) { return; } if(visibilityState === 'hidden') { if(gIsInMultiView && gMultiUnmuteOnHover && gMultiHoverBlock) { if(!gMultiIframeMode) { const elVideo = gMultiHoverBlock.querySelector('video'); if(!elVideo.muted) { elVideo.muted = true; } } else { multiSetIframeVolume(gMultiHoverBlock, true, gMultiHoverBlock._data.volume.volume); } } if(!getSetting('blur-mute')) { return; } if(gIsInMultiView) { document.querySelectorAll('video').forEach(video => { video._wasMuted = video.muted; video.muted = true; }); gWasMutedBySetting = true; return; } const elVideo = getCurrentVideo(); if(!elVideo) { return; } if(document.pictureInPictureElement) { return; } if(elVideo.muted) { return; } blurMuteVideo(elVideo); } else if(visibilityState === 'visible') { if(!gWasMutedBySetting) { return; } gWasMutedBySetting = false; if(!hasUserInteracted()) { return; } if(gIsInMultiView) { document.querySelectorAll('video').forEach(video => { video.muted = video._wasMuted; }); return; } if(getSetting('vm')) { return; } const elVideo = getCurrentVideo(); if(!elVideo) { return; } if(!elVideo.muted) { return; } elVideo.muted = false; } }); const elFooterInfo = document.createElement('div'); elFooterInfo.className = 'cb-enh-footer'; elFooterInfo.innerHTML = 'Chaturbate™ Enhancer v' + gVersion + ' © <a href="https://improper.dev/" target="_blank" rel="noopener" referrerpolicy="origin">improper.dev</a>'; document.querySelector('.langs').insertAdjacentElement('afterend', elFooterInfo); const elRecWarnModal = document.createElement('div'); elRecWarnModal.id = 'cb-enh-record-warn-modal'; elRecWarnModal.className = 'cb-enh-big-window'; elRecWarnModal.innerHTML = gRecordWarnModalHTML.replace('{{SVG_CLOSE_ICON}}', SVG_CLOSE_ICON); document.body.append(elRecWarnModal); document.addEventListener('fullscreenchange', onFullscreenChange, false); document.addEventListener('mozfullscreenchange', onFullscreenChange, false); document.addEventListener('MSFullscreenChange', onFullscreenChange, false); document.addEventListener('webkitfullscreenchange', onFullscreenChange, false); document.addEventListener('leavepictureinpicture', function() { document.querySelectorAll('video').forEach(elVideo => { playVideoIgnoreErrors(elVideo); }); }); localizeNextTick(); if(window.navigation) { window.navigation.addEventListener('navigatesuccess', () => { if(window.location.pathname === '/') { queryAndRun('a.GenderNav__button.active', el => { el.classList.remove('active'); }); queryAndRun('.GenderNav__button[data-testid="gender-nav-"]', el => { el.classList.add('active'); }); } if(window.location.pathname.startsWith('/followed-cams/')) { if(!gIntvUpdateFollowedList) { enhanceFollowedList(); } } else { gIntvUpdateFollowedList = clearIntervalEx(gIntvUpdateFollowedList); } exitVideoPiP(); convertNavButtonsToLinks(); const broadcasterName = getCurrentBroadcasterNameFromURL(); if(broadcasterName) { enhanceRoom(true, broadcasterName); } else { cleanupRoom(); updatePageTitle(); } }); } updatePageTitle(); }); function updatePageTitle() { if(isMultiViewPage()) { setPageTitle('Multi Cam Viewer - Chaturbate Enhancer'); return; } const pathData = getPathnameData(); let pathPageType = pathData.pageType; if(!pathPageType) { if(!gCurrentBroadcaster) { setPageTitle(null); } return; } pathPageType = pathPageType.replaceAll('-', '_'); let genderAppend = ''; if(pathData.gender) { genderAppend += '_' + pathData.gender; } const pageTitle = getLocale(`pt_${pathPageType}${genderAppend}`); if(!pageTitle) { if(!gCurrentBroadcaster) { setPageTitle(null); } return; } setPageTitle(pageTitle + ' - Chaturbate Enhancer'); } function navigateDynamic(url) { history.pushState({}, '', url); window.dispatchEvent(new PopStateEvent('popstate', { state: {} })); } function canBeDynamicallyNavigatedFrom() { if(gCurrentRoomIsInaccessible) { return false; } if(isMultiViewPage()) { return false; } const prefixedPages = [ '/tags/', '/discover/', '/my_collection/', '/messages/', '/p/', '/b/' ]; for(const v of prefixedPages) { if(window.location.pathname.startsWith(v)) { return false; } } if(isRandomPage() && window.location.pathname.match(/^\/[^\/]+\/([^\/]+)\//)) { return false; } return true; } const LONG_GENDER_NAMES_TO_SHORT = { 'female': 'f', 'male': 'm', 'couple': 'c', 'trans': 't' }; const SHORT_GENDER_NAMES_TO_LONG = { 'f': 'female', 'm': 'male', 'c': 'couple', 't': 'trans' }; function getLongGenderName(gender) { return SHORT_GENDER_NAMES_TO_LONG[gender]; } function getShortGenderName(gender) { return LONG_GENDER_NAMES_TO_SHORT[gender]; } function getCurrentPreferredGender() { if(window.location.pathname === '/') { return null; } if(gCurrentBroadcasterGender) { return getShortGenderName(gCurrentBroadcasterGender); } const elNavBtn = document.querySelector('a.GenderNav__button.active'); if(elNavBtn) { const testId = elNavBtn.getAttribute('data-testid'); if(testId && testId.startsWith('gender-nav-')) { return testId.substring('gender-nav-'.length); } } return null; } function getPathnameData() { const pathname = window.location.pathname; const genderPageMatch = pathname.match(/^\/(female|male|couple|trans)-cams(?:\/|$)/); const genderMatch = genderPageMatch || pathname.match(/^\/(?:followed-cams|spy-on-cams|tags|discover|random)\/(female|male|couple|trans)(?:\/|$)/); const pageTypeMatch = pathname.match(/^\/(followed-cams|spy-on-cams|tags|discover|random)(?:\/|$)/); let pageType = null; if(pageTypeMatch) { pageType = pageTypeMatch[1]; } else if(pathname === '/' || genderPageMatch) { pageType = 'index'; } return { gender: genderMatch ? getShortGenderName(genderMatch[1]) : null, pageType: pageType }; } function getGendersForRandomPage(basedOnUrl) { const m = basedOnUrl.match(/^\/[^\/]+\/([^\/]+)\//); if(!m) { return; } return getShortGenderName(m[1]); } function getHiddenGenders() { return getSetting('featured-hidden-genders') || []; } function getFeaturedGenders() { const hiddenGenders = getHiddenGenders(); const featuredGenders = []; for(const v of ['f', 'c', 'm', 't']) { if(!hiddenGenders.includes(v)) { featuredGenders.push(v); } } return featuredGenders; } function multiGetRandomGenders() { const randomGenders = getSetting('multi-random-genders'); if(randomGenders && randomGenders.length !== 0) { return randomGenders; } return getFeaturedGenders(); } function getRandomRoom(callback, genders) { getJSON_noNative('https://cb-enh-api.improper.dev/api/random?key=' + acre3 + '&g=' + genders.join(','), (data) => { if(data.name) { callback(data); } }); } function goToRandomRoom(newtab, ajax, basedOnUrl = null) { if(!basedOnUrl) { basedOnUrl = document.getElementById('cb-enh-random-cam')?.getAttribute('href'); } let genders = []; const prefGender = getGendersForRandomPage(basedOnUrl); if(prefGender) { genders = [prefGender]; } else { genders = getFeaturedGenders(); } getRandomRoom((data) => { if(data.name) { const url = '/' + data.name + '/'; if(newtab) { openInNewTab(url); return; } if(ajax && canBeDynamicallyNavigatedFrom()) { navigateDynamic(url); return; } document.location.href = url; } }, genders); } let gBigPreviewEnabled = true; let gBigPreviewShown = false; let gBigPreviewHLS = null; let gBigPreviewUsername = null; let gBigPreviewSession = 0; let gBigPreviewWasLoadedFromCache = false; let gIntvBigPreviewPreloadHLS = null; let gBigPreviewVideo = null; let gBigPreviewRoomCard = null; let gBigPreviewReq = null; let gBigPreviewReqs = {}; let gBigPreviewPosterReq = null; let gBigPreviewPlayAudio = true; const gBigPreviewPrestart = true; const BIG_PREVIEW_MAX_RESOLUTION = 720; const BIG_PREVIEW_CACHE_MAX_ROWS = 1000; let gBigPreviewDB = null; if(!gIsFullVideoPage) { const bigPreviewDBOpenReq = indexedDB.open('enh_hls_urls', 1); bigPreviewDBOpenReq.onupgradeneeded = function(e) { gBigPreviewDB = e.target.result; const store = gBigPreviewDB.createObjectStore('hls_urls', { keyPath: 'username' }); store.createIndex('lastUpdate', 'lastUpdate'); }; bigPreviewDBOpenReq.onsuccess = function(e) { gBigPreviewDB = e.target.result; }; } function isBigPreviewCurrent(username, session) { return gBigPreviewUsername === username && gBigPreviewSession === session; } function bigPreviewHide() { gIntvBigPreviewPreloadHLS = clearTimeoutEx(gIntvBigPreviewPreloadHLS); if(!gBigPreviewShown) { return; } queryHideElement('#cb-enh-room-preview'); if(gBigPreviewRoomCard) { gBigPreviewRoomCard.querySelector('a[data-room]').prepend(gBigPreviewVideo); if(isVideoPlaying(gBigPreviewVideo)) { gBigPreviewRoomCard.querySelector('img.RoomCardThumbnail__image, img.room_thumbnail, img.FollowedDropdown__room-image').style.opacity = '0'; } } if(gBigPreviewHLS && !gBigPreviewPrestart) { bigPreviewUnloadVideo(); } if(gBigPreviewPrestart) { const elVideo = document.getElementById('cb-enh-room-preview-video'); elVideo.muted = true; } if(gBigPreviewPosterReq && !gBigPreviewPrestart) { gBigPreviewPosterReq.abort(); } gBigPreviewShown = false; } function bigPreviewUnloadVideo() { const previewSession = ++gBigPreviewSession; gIntvBigPreviewPreloadHLS = clearTimeoutEx(gIntvBigPreviewPreloadHLS); if(gBigPreviewReq) { abortRequest(gBigPreviewReq); gBigPreviewReq = null; } gBigPreviewReqs = {}; if(gBigPreviewRoomCard) { const elRoomCard = gBigPreviewRoomCard; const elThumb = elRoomCard.querySelector('img.RoomCardThumbnail__image, img.room_thumbnail, img.FollowedDropdown__room-image'); if(elThumb) { let capturedFrame = false; try { gLastLoadedThumbReqTime = Date.now(); const canvas = gBigPreviewVideo ? captureVideoFrame(gBigPreviewVideo) : null; if(canvas) { const frameSrc = canvas.toDataURL('image/png'); canvas.className = 'cb-enh-room-preview-frame'; elRoomCard.querySelector('.cb-enh-room-preview-frame')?.remove(); gBigPreviewVideo.after(canvas); elThumb.classList.add('cb-enh-room-preview-thumbnail'); elThumb.onload = null; elThumb.style.opacity = '0'; elThumb.src = frameSrc; const showThumbnail = () => { if(gBigPreviewSession === previewSession || gBigPreviewRoomCard !== elRoomCard || gBigPreviewShown || !isVideoPlaying(gBigPreviewVideo)) { elThumb.style.opacity = '1'; } requestAnimationFrame(() => { requestAnimationFrame(() => canvas.remove()); }); }; elThumb.decode().then(showThumbnail, showThumbnail); capturedFrame = true; } } catch(e) { } if(!capturedFrame) { elThumb.style.opacity = '1'; } } gBigPreviewRoomCard = null; } if(gBigPreviewHLS) { gBigPreviewHLS.stopLoad(); gBigPreviewHLS.detachMedia(); gBigPreviewHLS.destroy(); gBigPreviewHLS = null; } const elVideo = document.getElementById('cb-enh-room-preview-video'); if(elVideo && elVideo.src && elVideo.src.startsWith('blob:')) { URL.revokeObjectURL(elVideo.src); elVideo.removeAttribute('src'); elVideo.load(); } const elImg = document.getElementById('cb-enh-room-preview-img'); if(elImg) { elImg.removeAttribute('src'); hideElement(elImg); } const elPoster2 = document.getElementById('cb-enh-room-preview-img2'); if(elPoster2) { elPoster2.removeAttribute('src'); hideElement(elPoster2); } gBigPreviewUsername = null; } function bigPreviewLoadVideo(username, elPreviewImg) { const previewSession = gBigPreviewSession; if(elPreviewImg) { const elPoster1 = document.getElementById('cb-enh-room-preview-img'); elPoster1.replaceWith(elPreviewImg); elPreviewImg.id = 'cb-enh-room-preview-img'; showElement(elPreviewImg); } const elPoster2 = document.getElementById('cb-enh-room-preview-img2'); let level_url = null; if(gBigPreviewDB) { const tx = gBigPreviewDB.transaction('hls_urls', 'readonly'); const store = tx.objectStore('hls_urls'); const dbReq = store.get(username); dbReq.onsuccess = function(e) { if(!isBigPreviewCurrent(username, previewSession) || !e.target.result) { return; } gBigPreviewWasLoadedFromCache = true; const hls_source = e.target.result.url; level_url = e.target.result.lurl; let url_to_play = hls_source; if(level_url) { url_to_play = level_url; url_to_play = hls_source.replace('/playlist.m3u8', '/chunklist_w') + level_url + '.m3u8'; } bigPreviewSetVideo(url_to_play); } } if(gBigPreviewPosterReq) { abortRequest(gBigPreviewPosterReq); gBigPreviewPosterReq = null; } gBigPreviewPosterReq = xmlhttpRequest_tryNative({ method: 'GET', url: 'https://jpeg.live.mmcdn.com/stream?room=' + username + '&f=' + Math.random(), timeout: 10000, responseType: 'arraybuffer', onload: function(req) { if(!isBigPreviewCurrent(username, previewSession)) { return; } if([200, 204].includes(req.status) && req.response.byteLength !== 0) { elPoster2.src = 'data:image/jpg;base64,' + btoa(String.fromCharCode.apply(null, new Uint8Array(req.response))); showElement(elPoster2); } gBigPreviewPosterReq = null; }, onerror: function() { if(!isBigPreviewCurrent(username, previewSession)) { return; } gBigPreviewPosterReq = null; } }); } function bigPreviewShow(username, elPreviewImg, elRoomCard) { document.getElementById('cb-enh-room-preview-preview').append(gBigPreviewVideo); const elRoomCardThumb = gBigPreviewRoomCard.querySelector('img.RoomCardThumbnail__image, img.room_thumbnail, img.FollowedDropdown__room-image'); const canvas = captureVideoFrame(gBigPreviewVideo); if(canvas) { const dataURL = canvas.toDataURL('image/png'); elRoomCardThumb.src = dataURL; } elRoomCardThumb.style.opacity = '1'; gBigPreviewUsername = username; gBigPreviewShown = true; const roomCardRect = elRoomCard.getBoundingClientRect(); const previewWidth = document.documentElement.clientWidth / 2.25; const previewHeightImg = previewWidth * 9 / 16; const previewHeightTotal = previewHeightImg + 90; const offsetXRight = 12; const offsetX = 4; const offsetY = 4; let posX = roomCardRect.left + offsetXRight; if(posX + previewWidth > document.documentElement.clientWidth) { posX = roomCardRect.left - offsetX - previewWidth; } let posY = roomCardRect.top + (roomCardRect.height / 2) - (previewHeightTotal / 2); if(posY < offsetY) { posY = offsetY; } if(posY + previewHeightTotal > document.documentElement.clientHeight - offsetY) { posY = document.documentElement.clientHeight - previewHeightTotal - offsetY; } const elPreview = document.getElementById('cb-enh-room-preview'); elPreview.style.left = posX + 'px'; elPreview.style.top = posY + 'px'; elPreview.style.width = previewWidth + 'px'; elPreview.style.height = previewHeightTotal + 'px'; const elPreview2 = document.getElementById('cb-enh-room-preview-preview'); elPreview2.style.width = previewWidth + 'px'; elPreview2.style.height = previewHeightImg + 'px'; if(!gBigPreviewPrestart) { gBigPreviewWasLoadedFromCache = false; bigPreviewLoadVideo(username, elPreviewImg); } else { const elVideo = document.getElementById('cb-enh-room-preview-video'); elVideo.muted = !gBigPreviewPlayAudio || !hasUserInteracted() || getSetting('vm'); } let subject = elRoomCard.querySelector('.RoomCardSubject, .subject').innerText; if(subject == '') { subject = '\u00A0'; } document.getElementById('cb-enh-room-preview-username').innerText = gBigPreviewUsername; document.getElementById('cb-enh-room-preview-info-age').innerText = elRoomCard.querySelector('.age').innerText; document.getElementById('cb-enh-room-preview-subject').innerText = subject; const elLocation = elRoomCard.querySelector('.location'); if(elLocation) { document.getElementById('cb-enh-room-preview-location').innerText = elLocation.innerText; } document.getElementById('cb-enh-room-preview-caminfo').innerText = elRoomCard.querySelector('li.cams .time').innerText + elRoomCard.querySelector('li.cams .comma').innerText + elRoomCard.querySelector('li.cams .viewers').innerText; const elGenderImg = elRoomCard.querySelector('span[data-testid="room-card-gender"]'); const elGenderImg2 = elGenderImg.cloneNode(true); document.getElementById('cb-enh-room-preview-info-gender').replaceChildren(elGenderImg2); clearElement(document.getElementById('cb-enh-room-preview-info-flag')); const elFlag = elRoomCard.querySelector('.thumbnail_flag'); if(elFlag) { const elFlag2 = elFlag.cloneNode(true); document.getElementById('cb-enh-room-preview-info-flag').replaceChildren(elFlag2); } queryShowElement('#cb-enh-room-preview'); } function bigPreviewSetVideo(hls_source) { const username = gBigPreviewUsername; const previewSession = gBigPreviewSession; const elVideo = document.getElementById('cb-enh-room-preview-video'); elVideo.muted = !gBigPreviewPlayAudio || !hasUserInteracted() || !gBigPreviewShown || getSetting('vm'); if(gBigPreviewHLS) { gBigPreviewHLS.stopLoad(); gBigPreviewHLS.detachMedia(); gBigPreviewHLS.loadSource(hls_source); gBigPreviewHLS.attachMedia(elVideo); return; } gBigPreviewHLS = createHLS(); gBigPreviewHLS.config.preserveManualLevelOnError = getSetting('error-quality') === false; gBigPreviewHLS.on(Hls.Events.MANIFEST_PARSED, function() { if(gBigPreviewHLS !== this || !isBigPreviewCurrent(username, previewSession)) { return; } if(this.levels.length > 1) { multiUpdateVideoQuality(this, BIG_PREVIEW_MAX_RESOLUTION); this.currentLevel = this.autoLevelCapping; } this.startLoad(); playVideoIgnoreErrors(elVideo); if(this.levels.length > 1) { let levelUrl = cutLevelUrl(this.levels[this.autoLevelCapping].url[0]); bigPreviewSetCachedHLSURL(username, this.url, levelUrl); } }); gBigPreviewHLS.loadSource(hls_source); gBigPreviewHLS.attachMedia(elVideo); } function bigPreviewSetCachedHLSURL(username, hls_source, level_url = null) { if(!gBigPreviewDB) { return; } if(hls_source.indexOf('/playlist.m3u8') === -1 && hls_source.indexOf('/playlist_sfm4s.m3u8') === -1) { return; } const tx = gBigPreviewDB.transaction('hls_urls', 'readwrite'); const store = tx.objectStore('hls_urls'); const req = store.get(username); req.onsuccess = e => { const row = e.target.result || { username }; let level_url_write = level_url !== null ? level_url : row.lurl; store.put({ username, lastUpdate: Date.now(), url: hls_source, lurl: level_url_write }); }; } function bigPreviewCacheClearAboveLimit() { if(!gBigPreviewDB) { return; } const tx = gBigPreviewDB.transaction('hls_urls', 'readwrite'); const store = tx.objectStore('hls_urls'); const idx = store.index('lastUpdate'); const countReq = store.count(); countReq.onerror = () => rej(countReq.error); countReq.onsuccess = () => { let extra = countReq.result - BIG_PREVIEW_CACHE_MAX_ROWS; if(extra <= 0) { return; } const curReq = idx.openCursor(); curReq.onerror = () => rej(curReq.error); curReq.onsuccess = e => { const cursor = e.target.result; if(!cursor || extra <= 0) { return; } cursor.delete(); extra--; cursor.continue(); } }; } function bigPreviewPreloadHLS(username, elPreviewImg) { const previewSession = ++gBigPreviewSession; gBigPreviewUsername = username; gBigPreviewWasLoadedFromCache = false; if(elPreviewImg) { const elPoster1 = document.getElementById('cb-enh-room-preview-img'); elPoster1.replaceWith(elPreviewImg); elPreviewImg.id = 'cb-enh-room-preview-img'; showElement(elPreviewImg); bigPreviewLoadVideo(username, elPreviewImg); } if(gBigPreviewReq) { abortRequest(gBigPreviewReq); gBigPreviewReq = null; } gBigPreviewReqs = {}; gIntvBigPreviewPreloadHLS = clearTimeoutEx(gIntvBigPreviewPreloadHLS); const req = isRoomOnline(username, (isOnline) => { if(!isOnline) { return; } if(!isBigPreviewCurrent(username, previewSession)) { return; } gIntvBigPreviewPreloadHLS = clearTimeoutEx(gIntvBigPreviewPreloadHLS); gIntvBigPreviewPreloadHLS = setTimeout(() => { if(!isBigPreviewCurrent(username, previewSession)) { return; } const req = xmlhttpRequest_tryNative({ method: 'GET', url: '/api/chatvideocontext/' + username + '/', timeout: 60*1*1000, onload: function(req) { if(!isBigPreviewCurrent(username, previewSession)) { return; } delete gBigPreviewReqs[username]; gBigPreviewReq = false; let data; try { data = JSON.parse(req.responseText); } catch(e) { return; } if(!('room_status' in data)) { return; } const status = data['room_status']; if(status !== 'public') { if(gBigPreviewUsername === username) { bigPreviewHide(); } } else if(!('hls_source' in data) || data['hls_source'] === '') { if(gBigPreviewUsername === username) { bigPreviewHide(); } } else { const hls_source = cutPlaylistUrl(data['hls_source']); let updateVideo = false; if(!gBigPreviewWasLoadedFromCache && gBigPreviewUsername === username) { updateVideo = true; } else if(gBigPreviewUsername === username && gBigPreviewHLS && gBigPreviewHLS.url && getCBTrans(hls_source) !== getCBTrans(gBigPreviewHLS.url)) { updateVideo = true; } const hlsObj = createHLS(); hlsObj.on(Hls.Events.MANIFEST_PARSED, function() { if(!isBigPreviewCurrent(username, previewSession)) { this.destroy(); return; } multiUpdateVideoQuality(this, BIG_PREVIEW_MAX_RESOLUTION); if(this.autoLevelCapping === -1) { this.autoLevelCapping = this.levels.length - 1; } if(updateVideo) { this.currentLevel = this.autoLevelCapping; if(gBigPreviewHLS) { gBigPreviewHLS.stopLoad(); gBigPreviewHLS.detachMedia(); gBigPreviewHLS.destroy(); } gBigPreviewHLS = this; const elVideo = document.getElementById('cb-enh-room-preview-video'); if(elVideo.src && elVideo.src.startsWith('blob:')) { URL.revokeObjectURL(elVideo.src); elVideo.removeAttribute('src'); elVideo.load(); } elVideo.muted = !gBigPreviewPlayAudio || !hasUserInteracted() || !gBigPreviewShown || getSetting('vm'); this.attachMedia(elVideo); this.startLoad(); playVideoIgnoreErrors(elVideo); } const levelUrl = cutLevelUrl(this.levels[this.autoLevelCapping].url[0]); bigPreviewSetCachedHLSURL(username, hls_source, levelUrl); if(!updateVideo) { hlsObj.destroy(); } }); hlsObj.on(Hls.Events.ERROR, function(event, data) { if(!updateVideo || !isBigPreviewCurrent(username, previewSession)) { hlsObj.destroy(); } }); hlsObj.loadSource(hls_source); } }, onerror: function() { if(!isBigPreviewCurrent(username, previewSession)) { return; } delete gBigPreviewReqs[username]; gBigPreviewReq = false; }, onabort: function() { if(!isBigPreviewCurrent(username, previewSession)) { return; } delete gBigPreviewReqs[username]; } }); const reqOrTrue = req ? req : true; gBigPreviewReq = reqOrTrue; gBigPreviewReqs[username] = reqOrTrue; }, 250); }); const reqOrTrue = req ? req : true; gBigPreviewReq = reqOrTrue; gBigPreviewReqs[username] = reqOrTrue; } window.addEventListener('blur', function() { bigPreviewHide(); if(gBigPreviewPrestart) { bigPreviewUnloadVideo(); } const elQualityLevel = document.querySelector('div[data-testid="quality-option"]'); if(elQualityLevel) { const elQualityLevelSelector = elQualityLevel.parentElement; if(elQualityLevelSelector) { if(elQualityLevelSelector.style.visibility !== 'hidden') { const elQualityBtn = document.querySelector('div[data-testid="video-quality-btn"]'); if(elQualityBtn) { elQualityBtn.click(); } } } } } ); function maybeUnmuteCurrentVideo() { if(getSetting('vm') !== true) { unmuteCurrentVideo(); } } function unmuteCurrentVideo() { if(!hasUserInteracted()) { return; } const elVideo = getCurrentVideo(); if(!elVideo) { return; } if(!elVideo.muted) { return; } const btn = document.querySelector('div.hover-btn[aria-label="Unmute"]'); if(!btn) { return; } btn?.click(); } window.addEventListener('focus', function() { if(gUnmuteCurrentVideoOnFocus) { gUnmuteCurrentVideoOnFocus = false; if(getSetting('vm') !== true && hasUserInteracted()) { makeSureRuns(unmuteCurrentVideo); } return; } } ); function updateSettingsWindow() { let settingsContentBlocklistUsernameHTMLFinal = ''; const blData = blocklistLoadData(); blData['list'].forEach(function(username) { settingsContentBlocklistUsernameHTMLFinal += settingsContentBlocklistUsernameHTML.replaceAll('{{username}}', username); }); document.getElementById('cb-enh-settings-blocklist-list').innerHTML = settingsContentBlocklistUsernameHTMLFinal; const elBlocklistInfo = document.getElementById('cb-enh-settings-blocklist-info'); if(blData['list'].length !== 0) { elBlocklistInfo.innerText = getLocale('set_blocklist_info_current') + ' ' + blData['list'].length + ' ' + getLocale('cams') + '.'; showElement(elBlocklistInfo); } else { hideElement(elBlocklistInfo); } queryToggleElement('#cb-enh-settings-blocklist-empty', blData['list'].length === 0); const blocklistCountries = getSetting('blocklist-cc') || []; let settingsContentBlocklistCountryHTMLFinal = ''; blocklistCountries.forEach((country) => { settingsContentBlocklistCountryHTMLFinal += settingsContentBlocklistCountryHTML.replaceAll('{{country_code}}', country).replaceAll('{{country_name}}', getLocale('cc_' + country)); }); document.getElementById('cb-enh-settings-blocklist-cc-list').innerHTML = settingsContentBlocklistCountryHTMLFinal; const elBlocklistCountryInfo = document.getElementById('cb-enh-settings-blocklist-cc-info'); if(blocklistCountries.length !== 0) { elBlocklistCountryInfo.innerText = getLocale('set_blocklist_info_current') + ' ' + blocklistCountries.length + ' ' + getLocale('countries') + '.'; showElement(elBlocklistCountryInfo); } else { hideElement(elBlocklistCountryInfo); } queryToggleElement('#cb-enh-settings-blocklist-cc-empty', blocklistCountries.length === 0); document.getElementById('cb-enh-settings-preview-rooms').checked = getSetting('animate_thumbnails'); document.getElementById('cb-enh-settings-preview-rooms-video').checked = !getSetting('disable-big-previews'); document.getElementById('cb-enh-settings-input-show-logo').checked = !getSetting('hide-vid-logo'); document.getElementById('cb-enh-settings-rewind').checked = getSetting('rewind') !== false; document.getElementById('cb-enh-settings-auto-rules').checked = getSetting('auto-chat-rules'); document.getElementById('cb-enh-settings-blur-mute').checked = getSetting('blur-mute'); document.getElementById('cb-enh-settings-duration').checked = getSetting('duration') !== false; document.getElementById('cb-enh-settings-inactive-quality').checked = getSetting('inactive-quality') !== false; document.getElementById('cb-enh-settings-error-quality').checked = getSetting('error-quality') !== false; document.getElementById('cb-enh-settings-inactive-load').checked = getSetting('inactive-load') !== false; document.getElementById('cb-enh-settings-chat-hide-notices').checked = getSetting('chat-hide-notices'); document.getElementById('cb-enh-settings-chat-hide-subject-notices').checked = getSetting('chat-hide-subject-notices'); document.getElementById('cb-enh-settings-chat-hide-tips').checked = getSetting('chat-hide-tips'); document.getElementById('cb-enh-settings-chat-hide-greys').checked = getSetting('chat-hide-greys'); document.getElementById('cb-enh-settings-rec-exit-warn').checked = getSetting('rec-exit-warn'); document.getElementById('cb-enh-settings-mute-big-previews').checked = getSetting('mute-big-previews'); document.getElementById('cb-enh-settings-small-margins').checked = getSetting('smargins'); document.getElementById('cb-enh-settings-rec-no-audio').checked = getSetting('rec-no-audio'); document.getElementById('cb-enh-settings-rec-no-video').checked = getSetting('rec-no-video'); document.getElementById('cb-enh-settings-rec-no-audio').disabled = gRecording; document.getElementById('cb-enh-settings-rec-no-video').disabled = gRecording; document.getElementById('cb-enh-settings-open-new-tab').checked = getSetting('open-new-tab'); document.getElementById('cb-enh-settings-no-socials').checked = getSetting('no-socials'); document.getElementById('cb-enh-settings-no-merch-links').checked = getSetting('no-merch-links'); document.getElementById('cb-enh-settings-no-surveys').checked = getSetting('no-surveys'); document.getElementById('cb-enh-multi-settings-time-format').value = getSetting('time-format') || 0; document.getElementById('cb-enh-multi-settings-rewind-time').value = getSetting('rewind-time') || 0; getHiddenGenders().forEach(function(v) { document.getElementById('cb-enh-settings-featuredg-' + v).checked = false; }); multiUpdateSettings(); localizeNextTick(); const elInfo = document.querySelector('.cb-enh-settings-group[data-group="info"]'); if(elInfo) { elInfo.innerHTML = ''; const msg = getSupportMessage(getUserType()); if(msg) { elInfo.innerHTML = msg; } } } function setPageOverlayShown(show) { queryToggleElement('#cb-enh-page-overlay', show); document.querySelector('body > #base').classList.toggle('cb-enh-base-blurred', show); document.documentElement.classList.toggle('cb-enh-html-noscroll', show); } function setSettingsOpen(open) { if(open) { document.querySelector('.cb-enh-settings-group-selector-group[data-group="general"]')?.click(); queryAndRun('.cb-enh-settings-group-selector-group[data-group="info"]', el => { if(getUserType() === 2) { hideElement(el); return; } el.style.display = 'inline-block'; }); updateSettingsWindow(); } document.getElementById('cb-enh-settings-window').style.display = open ? 'flex' : 'none'; const userType = getUserType(); const showSupportMsg = open && (userType === 0 || userType === 1); document.getElementById(gtrrft('blso')).classList.toggle('cb-enh-hidden', !showSupportMsg); setPageOverlayShown(open); } function setSmallPageMargins(enable) { document.body.classList.toggle('cb-enh-small-margins', enable); } function setVideoLogoVisible(enable) { document.body.classList.toggle('cb-enh-hvl', !enable); } function initAnimatedThumbs() { document.querySelectorAll('#animate_thumbnails_form label').forEach(el => el.removeAttribute('style')); document.querySelector('#animate_thumbnails_form .disabledTooltipColor')?.remove(); document.querySelectorAll('#animate_thumbnails_form input').forEach(el => el.removeAttribute('disabled')); document.querySelectorAll('#animate_thumbnails_form input').forEach(el => el.removeAttribute('readonly')); const elAnimateThumbs = document.getElementById('id_animate_thumbnails'); if(elAnimateThumbs) { elAnimateThumbs.style.cursor = 'inherit'; } let animateThumbnails = getSetting("animate_thumbnails"); if(animateThumbnails === null) { animateThumbnails = true; setSetting("animate_thumbnails", animateThumbnails); } const elCheckboxComponent = document.querySelector('#animate_thumbnails_form .checkboxComponent'); if(elCheckboxComponent) { elCheckboxComponent.classList.remove('disabled'); elCheckboxComponent.style.cursor = 'pointer'; elCheckboxComponent.classList.toggle('checked', animateThumbnails); } const elAnimThumbsForm = document.getElementById('animate_thumbnails_form'); if(elAnimThumbsForm) { elAnimThumbsForm.addEventListener('click', function(e) { elCheckboxComponent.classList.toggle('checked'); elCheckboxComponent.classList.remove('disabled'); const enable = elCheckboxComponent.classList.contains('checked'); setSetting("animate_thumbnails", enable); document.getElementById('cb-enh-settings-preview-rooms-video').checked = enable; document.getElementById('cb-enh-settings-preview-rooms-video').dispatchEvent(new Event('change', { bubbles: true })); e.preventDefault(); e.stopPropagation(); }); } document.addEventListener('mouseover', (e) => { const el = e.target.closest('.roomCard, .RoomCard, .FollowedDropdown__room'); if(!el) { return; } if(e.relatedTarget && el.contains(e.relatedTarget)) { return; } e.preventDefault(); if(gBigPreviewShown) { return; } if(gBigPreviewVideo && isVideoPlaying(gBigPreviewVideo)) { return; } if(!getSetting("animate_thumbnails")) { return; } let el2 = el.querySelector('img[data-testid="room-card-image"]'); let uname = null; if(el2) { uname = el2.parentElement.getAttribute('data-room'); } else { el2 = el.querySelector('img.FollowedDropdown__room-image'); const elUname = el.querySelector('.FollowedDropdown__room-username'); if(elUname) { uname = elUname.innerText; } } if(!uname) { return; } gIntvCurrentHover = clearIntervalEx(gIntvCurrentHover); updateRoomThumb(el2, uname); gIntvCurrentHover = setInterval(() => { updateRoomThumb(el2, uname); }, 20); }); document.addEventListener('mouseout', (e) => { const el = e.target.closest('.roomCard, .RoomCard, .FollowedDropdown__room'); if(!el) { return; } if(e.relatedTarget && el.contains(e.relatedTarget)) { return; } e.preventDefault(); gIntvCurrentHover = clearIntervalEx(gIntvCurrentHover); }); document.addEventListener('click', (e) => { if(e.target.closest('.roomCard, .RoomCard, .FollowedDropdown__room')) { gIntvCurrentHover = clearIntervalEx(gIntvCurrentHover); } }); } function isOfflineThumbnail(byteLength) { return [4824, 4456, 9505, 2688, 3189, 4411, 3847].includes(byteLength); } function updateRoomThumb(el, uname) { if(!el) { gIntvCurrentHover = clearIntervalEx(gIntvCurrentHover); return; } if(gBigPreviewShown) { return; } if(gBigPreviewVideo && isVideoPlaying(gBigPreviewVideo)) { return; } el.onload = null; const reqTime = Date.now(); let req = null; function onload() { if(reqTime < gLastLoadedThumbReqTime) { return; } if(gBigPreviewShown) { return; } if(gBigPreviewVideo && isVideoPlaying(gBigPreviewVideo)) { return; } if(isOfflineThumbnail(req.response.byteLength)) { return; } gLastLoadedThumbReqTime = reqTime; el.src = 'data:image/jpg;base64,' + btoa(String.fromCharCode.apply(null, new Uint8Array(req.response))); } const url = 'https://thumb.live.mmcdn.com/minifwap/' + uname + '.jpg?' + reqTime; if(gDoNeedAlternativeWindowApproach) { xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, responseType: 'arraybuffer', onload: function(v) { req = v; onload(); } }); } else { req = new XMLHttpRequest(); req.timeout = 10000; req.responseType = 'arraybuffer'; req.addEventListener('load', onload); req.open('GET', url); req.send(); } } function applyCookPatch() { let windowObject = window; if(typeof unsafeWindow !== 'undefined') { windowObject = unsafeWindow; } const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie'); if(descriptor && descriptor.set) { const _set = descriptor.set; Object.defineProperty(document, 'cookie', { get: descriptor.get, set(v) { if(windowObject.__cnh__2.acrtt9 || typeof v !== 'string' || !v.includes('affkey="')) { return _set.call(this, v); } } }); } } tryRunFunction(applyCookPatch); document.cookie = 'noads=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/'; document.cookie = 'noads=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.' + gBaseDomain; const acre9 = 'eJxtkE0KwjAQha8SssmmhWSkCjmGN7D5URFBp7OR0rubNxC7cfe9+X0zqxUbjb3y29vB2PR8QdZyPt2ghR/QkiEYCLjUCiwiM7N8CmLaRwG43HOvVCBPx9FPYzgYP0Wi6El37WULp45V/ayuDXHRODcY10wAJUOkPc4dWnvHdgBQD4CGotCgeUbi5xlJnfTHnS5FDm9xm92+ZjhF1w=='; function acre9a() { if(typeof unsafeWindow.__cnh__2 === 'undefined') { return; } unsafeWindow.__cnh__2.acrtt9 = true; [ '/', '/accounts/register/', '/accounts/', '/auth/login/', '/auth/' ].forEach(v => { document.cookie = 'fromaffiliate=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=' + v; document.cookie = 'in_hit=; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=' + v; document.cookie = 'affkey="' + acre9 + '"; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=' + v; document.cookie = 'fromaffiliate=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=' + v + '; domain=.' + gBaseDomain; document.cookie = 'in_hit=; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=' + v + '; domain=.' + gBaseDomain; document.cookie = 'affkey="' + acre9 + '"; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=' + v + '; domain=.' + gBaseDomain; }); unsafeWindow.__cnh__2.acrtt9 = false; } if(!gIsFullVideoPage) { setIntervalRunFirst(acre9a, 1000); } function cleanupRoom() { if(!gCurrentBroadcaster) { return; } setPageTitle(null); stopRecording(false); exitVideoPiP(); gIntvCheckIfRoomIsOnline = clearIntervalEx(gIntvCheckIfRoomIsOnline); gIntvCheckIfRoomIsOnline_2 = clearIntervalEx(gIntvCheckIfRoomIsOnline_2); gIntvAcceptRules = clearIntervalEx(gIntvAcceptRules); gIntvWaitVideo = clearIntervalEx(gIntvWaitVideo); gIntvWaitBuyBox = clearIntervalEx(gIntvWaitBuyBox); gIntvWaitBuyBox2 = clearIntervalEx(gIntvWaitBuyBox2); gIntvPatchVideo = clearIntervalEx(gIntvPatchVideo); gIntvPatchHLS = clearIntervalEx(gIntvPatchHLS); gIntvScheduleUpdateCurrentHour = clearIntervalEx(gIntvScheduleUpdateCurrentHour); gIntvWaitForPlayBtn = clearIntervalEx(gIntvWaitForPlayBtn); gIntvWaitBioTable = clearIntervalEx(gIntvWaitBioTable); gIntvWaitMoreRooms = clearIntervalEx(gIntvWaitMoreRooms); destroyVideoDurationCounters(); rewindDestroyAll(); document.querySelector('.cb-enh-row')?.remove(); document.getElementById('cb-enh-duration')?.remove(); destroyBelowVideoButtons(); document.querySelectorAll('.cb-enh-avatar').forEach((el) => { el.remove(); }); document.querySelectorAll('.cb-enh-offline-snapshot').forEach((el) => { el.remove(); }); const elVolumeSlider = document.querySelector('div[data-testid="volume-slider"]'); if(elVolumeSlider) { elVolumeSlider.removeEventListener('wheel', volumeWheelHandler); } const elVideoHolder = document.getElementById('TheaterModeRoomContents'); if(elVideoHolder) { resetVideoFilters(elVideoHolder); resetVideoAudioBoost(elVideoHolder); } gCurrentBroadcaster = null; gCurrentBroadcasterGender = null; disableAllFreezoom(); } function enhanceRoom(ajaxTransition = false, broadcasterName = null) { const wasLoadedFromNavigationEvent = broadcasterName !== null; bigPreviewUnloadVideo(); cleanupRoom(); gIntvCurrentHover = clearIntervalEx(gIntvCurrentHover); if(!ajaxTransition && unsafeWindow.initialRoomDossier === '') { enhanceInaccessibleRoom(); return; } let userData; if(!ajaxTransition) { userData = JSON.parse(unsafeWindow.initialRoomDossier); broadcasterName = userData.broadcaster_username; if(userData['room_status'] === 'offline') { function callbackCheckOnline(data) { if(data['room_status'] !== 'offline') { gIntvCheckIfRoomIsOnline = clearIntervalEx(gIntvCheckIfRoomIsOnline); gIntvCheckIfRoomIsOnline_2 = clearIntervalEx(gIntvCheckIfRoomIsOnline_2); window.location.reload(); return; } } gIntvCheckIfRoomIsOnline = setInterval(() => { isRoomOnline(broadcasterName, (isOnline) => { if(!isOnline) { return; } getJSON_noNative('/api/biocontext/' + broadcasterName + '/', callbackCheckOnline, gHbx !== null); }); }, 500); gIntvCheckIfRoomIsOnline_2 = setInterval(() => { getJSON_noNative('/api/biocontext/' + broadcasterName + '/', callbackCheckOnline, gHbx !== null); }, 5000); } if(typeof unsafeWindow.hlsInstance === 'undefined') { gUnmuteCurrentVideoOnFocus = true; gIntvWaitForPlayBtn = clearIntervalEx(gIntvWaitForPlayBtn); gIntvWaitForPlayBtn = setInterval(() => { if(typeof unsafeWindow.hlsInstance !== 'undefined') { gIntvWaitForPlayBtn = clearIntervalEx(gIntvWaitForPlayBtn); return; } const elPlay = document.querySelector('#VideoPanel img[src="https://web.static.mmcdn.com/tsdefaultassets/play-inactive.svg"]'); if(!elPlay) { return; } gIntvWaitForPlayBtn = clearIntervalEx(gIntvWaitForPlayBtn); elPlay.click(); }, 100); } } else { if(!wasLoadedFromNavigationEvent) { broadcasterName = getCurrentBroadcasterName(); } stopRecording(false); } gCurrentBroadcaster = broadcasterName; gCurrentRoomIsInaccessible = false; setPageTitle(capitalize(gCurrentBroadcaster) + ' - Chaturbate Enhancer'); initSupportInfo(); let lastVideoEl = null; let lastVideoHolderEl = null; gIntvPatchVideo = clearIntervalEx(gIntvPatchVideo); gIntvPatchVideo = setIntervalRunFirst(() => { if(wasLoadedFromNavigationEvent && broadcasterName !== getCurrentBroadcasterName()) { return; } const elVideo = getCurrentVideo(); if(!elVideo) { setFreezoomEnabled(lastVideoHolderEl, false, lastVideoEl); lastVideoEl = null; lastVideoHolderEl = null; rewindDestroy(gRewindObjs[broadcasterName]); return; } if(elVideo === lastVideoEl) { return; } setFreezoomEnabled(lastVideoHolderEl, false, lastVideoEl); const elVideoHolder = getClosestVideoHolder(elVideo); lastVideoEl = elVideo; lastVideoHolderEl = elVideoHolder; maybeUnmuteCurrentVideo(); setFreezoomEnabled(elVideoHolder, true); updateVideoAudioBoost(elVideoHolder); elVideo.addEventListener('volumechange', (e) => { updateVolumeSettings(elVideo); }); }, 50); let lastHLSObj = null; gIntvPatchHLS = clearIntervalEx(gIntvPatchHLS); gIntvPatchHLS = setIntervalRunFirst(() => { if(typeof unsafeWindow.hlsInstance !== 'undefined' && lastHLSObj !== unsafeWindow.hlsInstance && unsafeWindow.hlsInstance && unsafeWindow.hlsInstance.levels && unsafeWindow.hlsInstance.levels.length !== 0) { if(wasLoadedFromNavigationEvent && broadcasterName !== getCurrentBroadcasterName()) { return; } lastHLSObj = unsafeWindow.hlsInstance; updateFakeVisibility(); const preserveManualLevelOnError = getSetting('error-quality') === false; unsafeWindow.__cnh__fnc__.patchHLS(preserveManualLevelOnError); if(!gDoNeedAlternativeWindowApproach) { if(unsafeWindow.hlsInstance.url) { let levelIndex = multiUpdateVideoQuality(unsafeWindow.hlsInstance, BIG_PREVIEW_MAX_RESOLUTION, false); if(levelIndex === null && unsafeWindow.hlsInstance.levels && unsafeWindow.hlsInstance.levels.length !== 0) { levelIndex = unsafeWindow.hlsInstance.levels.length - 1; } let levelUrl = null; if(levelIndex !== null) { levelUrl = cutLevelUrl(unsafeWindow.hlsInstance.levels[levelIndex].url[0]); } bigPreviewSetCachedHLSURL(gCurrentBroadcaster, unsafeWindow.hlsInstance.url, levelUrl); } } rewindDestroy(gRewindObjs[broadcasterName]); rewindInit0(); } }, 50); const lang = getSiteLang(); gIntvWaitBioTable = setInterval(() => { if(wasLoadedFromNavigationEvent && broadcasterName !== getCurrentBroadcasterName()) { return; } const elTable = document.querySelector('.BioContents > div > table'); if(!elTable) { return; } gIntvWaitBioTable = clearIntervalEx(gIntvWaitBioTable); if(gHbx) { if(elTable.childNodes.length >= 3) { const elRow = addBioRow('Info'); setBioRowValue(elRow, getLocale('r_bn')) elTable.childNodes[2].parentNode.insertBefore(elRow, elTable.childNodes[2]); showElement(elRow); } } const elFollowers = document.querySelector('td[data-testid="bio-tab-followers-value"]'); if(elFollowers) { const followers = formatNumber(elFollowers.innerText); if(followers !== 'NaN' && followers !== NaN && followers !== null) { elFollowers.innerText = followers; } } updateBioRows(); const elOfflineNotice_0 = document.querySelector('.offlineContentContainer'); if(elOfflineNotice_0.style.display !== 'none') { const elOfflineNotice = document.querySelector('.offlineRoomNotice'); if(elOfflineNotice) { if(gHbx) { window.scrollTo(0, 0); } insertRoomAv(elOfflineNotice, broadcasterName); fetchRoomSnapshot(broadcasterName, function(resp, url) { const img = document.createElement('img'); img.classList.add('cb-enh-offline-snapshot'); img.src = 'data:image/jpg;base64,' + btoa( String.fromCharCode.apply(null, new Uint8Array(resp)) ); img.src = url; elOfflineNotice.appendChild(img); }); } } const elRowName = document.querySelector('td[data-testid="bio-tab-real-name-value"]')?.parentElement; const elRowAge = document.querySelector('td[data-testid="bio-tab-age-value"]')?.parentElement; const elRowBdate = document.querySelector('td[data-testid="bio-tab-birth-date-value"]')?.parentElement; const elRowGender = document.querySelector('td[data-testid="bio-tab-i-am-value"]')?.parentElement; const elRowFollowers = document.querySelector('td[data-testid="bio-tab-followers-value"]')?.parentElement; if(elRowAge && elRowBdate) { elRowBdate.parentNode.insertBefore(elRowAge, elRowBdate); } if(elRowGender && elRowFollowers) { elRowGender.parentNode.insertBefore(elRowFollowers, elRowGender.nextSibling); } const elLastBroadCast = document.querySelector('tr > td[data-testid="bio-tab-last-broadcast-value"]')?.parentElement; const elOnlineForRow = addBioRow('Online For'); const elNextBroadcastRow = addBioRow('Next Broadcast'); const elLastSubjectRow = addBioRow('Last Subject'); if(elLastBroadCast) { elLastBroadCast.parentNode.insertBefore(elLastSubjectRow, elLastBroadCast.nextSibling); elLastBroadCast.parentNode.insertBefore(elNextBroadcastRow, elLastBroadCast.nextSibling); } const elCountryRow = addBioRow('Country'); const elRegionRow = addBioRow('Region'); const elScheduleRow = addBioRow('Schedule'); const elJoinedRow = addBioRow('Joined'); const elRowPrv5 = addBioRow('Private show price'); const elRowPrv4 = addBioRow('Private minimum'); const elRowPrvPremPrice = addBioRow('Premium private show price'); const elRowPrvPremMinimum = addBioRow('Premium private minimum'); const elRowPrv3 = addBioRow('Private spy price'); const elRowPrv2 = addBioRow('Private spy price (fanclub)'); const elRowPrv1 = addBioRow('Privates auto recording'); const elRowPrv0 = addBioRow('Private shows'); const elRowFanclubPrice = addBioRow('Fanclub price'); function populateBio(userData) { if(!userData.opt_out) { xmlhttpRequest({ method: 'GET', url: 'https://cb-enh-api.improper.dev/api/room/' + broadcasterName + '?lang=' + lang + '&key=' + acre3, timeout: 10000, onload: function(resp) { if(broadcasterName !== gCurrentBroadcaster) { return; } let data; try { data = JSON.parse(resp.responseText); } catch(e) { return; } const elLocation = document.querySelector('tr > td[data-testid="bio-tab-location-value"]')?.parentElement; const elInterestedIn = document.querySelector('tr > td[data-testid="bio-tab-interested-in-value"]')?.parentElement; const elGender = document.querySelector('tr > td[data-testid="bio-tab-i-am-value"]')?.parentElement; if(data['region'] && data['region_id'] !== null) { const href = REGION_LINKS[data['region_id']]; if(href) { const elLink = document.createElement('a'); elLink.innerText = data['region']; elLink.href = href; elRegionRow.querySelector('.cb-enh-row-value').append(elLink); const insertBeforeOrder = [ elLocation, elInterestedIn, elGender ]; for(const el of insertBeforeOrder) { if(el && !isElementHidden(el)) { el.parentNode.insertBefore(elRegionRow, el.nextSibling); break; } } showElement(elRegionRow); } } if(data['country']) { const elVal = elCountryRow.querySelector('.cb-enh-row-value'); elVal.innerText = data['country']; if(data['cc']) { const elFlag = document.createElement('span'); elFlag.className = 'fi fi-' + data['cc']; elFlag.style.marginLeft = '4px'; elVal.append(elFlag); } const insertBeforeOrder = [ elLocation, elInterestedIn, elGender ]; for(const el of insertBeforeOrder) { if(el && !isElementHidden(el)) { el.parentNode.insertBefore(elCountryRow, el.nextSibling); break; } } showElement(elCountryRow); } if(data['has_schedule'] && 'schedule' in data) { const secondsOnline = data['seconds_online'] !== null ? data['seconds_online'] : 0; initSchedule(elScheduleRow, gCurrentBroadcaster, secondsOnline, data.schedule); if(!isOnline) { const nextBroadcastText = getNextBroadcastPrediction(data.schedule); if(nextBroadcastText) { setBioRowValue(elNextBroadcastRow, nextBroadcastText); showElement(elNextBroadcastRow); } } } if(data.joined) { setBioRowValue(elJoinedRow, data.joined); showElement(elJoinedRow); } localize(); } }); } if(userData.allow_private_shows) { let spInfo = ''; if(userData.spy_private_show_price) { spInfo = userData.spy_private_show_price + ' tk/min'; } else { spInfo = getLocale('disabled'); } setBioRowValue(elRowPrv1, userData.allow_show_recordings ? getLocale('yes') : getLocale('no')); showElement(elRowPrv1); if('fan_club_spy_private_show_price' in userData && userData.fan_club_spy_private_show_price !== null && userData.fan_club_spy_private_show_price !== userData.spy_private_show_price) { if(userData.fan_club_spy_private_show_price === 0) { setBioRowValue(elRowPrv2, getLocale('free')); } else { setBioRowValue(elRowPrv2, userData.fan_club_spy_private_show_price + ' tk/min'); } showElement(elRowPrv2); } setBioRowValue(elRowPrv3, spInfo); showElement(elRowPrv3); if('private_min_minutes' in userData && userData.private_min_minutes !== null) { setBioRowValue(elRowPrv4, userData.private_min_minutes + ' ' + getLocale('minutes')); showElement(elRowPrv4); } if('private_show_price' in userData && userData.private_show_price !== null) { setBioRowValue(elRowPrv5, userData.private_show_price + ' tk/min'); showElement(elRowPrv5); } if(userData.premium_private_price) { setBioRowValue(elRowPrvPremPrice, userData.premium_private_price + ' tk/min') showElement(elRowPrvPremPrice); if('premium_private_min_minutes' in userData) { setBioRowValue(elRowPrvPremMinimum, userData.premium_private_min_minutes + ' ' + getLocale('minutes')); showElement(elRowPrvPremMinimum); } } } else { setBioRowValue(elRowPrv0, getLocale('no')); showElement(elRowPrv0); } if(userData.room_status === 'offline' && userData.room_title) { setBioRowValue(elLastSubjectRow, userData.room_title); showElement(elLastSubjectRow); } if(userData.room_status !== 'offline' && 'start_timestamp' in userData) { const secondsOnline = Date.now() / 1000 - Number(userData['start_timestamp']); const elVideoPanel = document.getElementById('VideoPanel'); elOnlineForRow.querySelector('.cb-enh-row-value').id = 'cb-enh-online-time'; const elOnlineRowValue = document.getElementById('cb-enh-online-time'); if(elLastBroadCast) { elLastBroadCast.parentNode.insertBefore(elOnlineForRow, elLastBroadCast); } else { const elLocation = document.querySelector('tr > td[data-testid="bio-tab-location-value"]')?.parentElement; const elInterestedIn = document.querySelector('tr > td[data-testid="bio-tab-interested-in-value"]')?.parentElement; const elGender = document.querySelector('tr > td[data-testid="bio-tab-i-am-value"]')?.parentElement; const insertBeforeOrder = [ elRegionRow, elCountryRow, elLocation, elInterestedIn, elGender ]; for(const el of insertBeforeOrder) { if(el && !isElementHidden(el)) { el.parentNode.insertBefore(elOnlineForRow, el.nextSibling); break; } } } initVideoDurationCounter(broadcasterName, elVideoPanel, elOnlineRowValue, secondsOnline); showElement(elOnlineForRow); } gCurrentBroadcasterGender = userData.broadcaster_gender; updateRandomLink(); } if(userData) { populateBio(userData); } const isOnline = (userData && userData.room_status !== 'offline') || (!userData && !isCurrentRoomOffline()); const elFanclubBtn = document.querySelector('.fanclubButton'); if(elFanclubBtn && !isElementHidden(elFanclubBtn)) { getJSON_noNative('/api/biocontext/' + broadcasterName + '/', function(data) { if(!data.performer_has_fanclub) { return; } if(!('fan_club_cost' in data)) { return; } if(data.fan_club_cost === null) { return; } if(broadcasterName !== gCurrentBroadcaster) { return; } const elLink = elFanclubBtn?.querySelector('span[style]'); if(elLink) { elLink.textContent = elLink.textContent + ' (' + data.fan_club_cost + '/m)'; } const elRowFanclubPriceLink = document.createElement('a'); elRowFanclubPriceLink.classList.add('cb-enh-row-value-fanclub'); elRowFanclubPriceLink.innerText = data.fan_club_cost + ' tk / ' + getLocale('month'); elRowFanclubPrice.querySelector('.cb-enh-row-value').append(elRowFanclubPriceLink); showElement(elRowFanclubPrice); }, gHbx !== null); } else { setBioRowValue(elRowFanclubPrice, getLocale('no')); setBioRowName(elRowFanclubPrice, 'Fanclub'); showElement(elRowFanclubPrice); } if(!userData && !isAG()) { getJSON_noNative('/api/chatvideocontext/' + broadcasterName + '/', function(data) { if(broadcasterName !== gCurrentBroadcaster) { return; } populateBio(data); }, gHbx !== null); } if(gHbx) { window.scrollTo(0, 0); } localizeNextTick(); }, 50); initBelowVideoButtons(); gIntvWaitVideo = setIntervalRunFirst(function() { if(wasLoadedFromNavigationEvent && broadcasterName !== getCurrentBroadcasterName()) { return; } const eTheaterBtn = document.getElementById('theater-mode-icon'); if(!eTheaterBtn) { return; } gIntvWaitVideo = clearIntervalEx(gIntvWaitVideo); const elVolumeSlider = document.querySelector('div[data-testid="volume-slider"]'); if(elVolumeSlider) { elVolumeSlider.addEventListener('wheel', volumeWheelHandler); } updateFakeVisibility(); if('pictureInPictureEnabled' in document && !!document.pictureInPictureEnabled) { if(document.getElementById('cb-enh-pip')) { return false; } const eBthHolder = eTheaterBtn.parentElement; eBthHolder.insertAdjacentHTML('afterbegin', '<div class="hover-btn drop-shadow-container" id="cb-enh-pip">' + SVG_PIP_ICON + '<div class="no-drop-shadow video-controls-tooltip"><p class="ce-loc" data-ce-loc="pip">Picture in Picture</p></div></div>'); } return false; }, 50); autoAcceptRules(); } let gVideoDurationCounters = {}; function updateOnlineTime() { const now = Date.now() / 1000; for(const v of Object.values(gVideoDurationCounters)) { const onlineCounterStart = v[0]; const secondsOnline = v[1]; let seekDurationTo = v[2]; const seekPauseTime = v[3]; const elOnlineTime = v[4]; const elOnlineRowValue = v[5]; const secondsOnlineUpdated = secondsOnline + now - onlineCounterStart; if(elOnlineTime) { const currentTimeFormatted = formatTime(Math.floor(secondsOnlineUpdated)); if(seekPauseTime !== null && seekDurationTo === null) { seekDurationTo = 0; } if(seekDurationTo || seekPauseTime !== null) { let seekTime = secondsOnlineUpdated - seekDurationTo; if(seekPauseTime !== null) { const pauseDuration = now - seekPauseTime; seekTime -= pauseDuration; seekDurationTo += pauseDuration; } seekTime = Math.floor(seekTime); seekDurationTo = Math.floor(seekDurationTo); const seekTimeFormatted = formatTime(seekTime); const seekDurationToFormatted = formatTime(seekDurationTo); elOnlineTime.innerText = `${seekTimeFormatted} / ${currentTimeFormatted} (-${seekDurationToFormatted})`; } else { elOnlineTime.innerText = currentTimeFormatted; } } if(elOnlineRowValue) { elOnlineRowValue.innerText = formatTimePretty(Math.floor(secondsOnlineUpdated)); } } } function initVideoDurationCounter(username, elVideoHolder, elOnlineRowValue, secondsOnline) { if(gVideoDurationCounters[username]) { return; } const onlineCounterStart = Date.now() / 1000; let elVideoControls = elVideoHolder.querySelector('div[data-testid="video-quality-btn"]')?.parentElement; if(!elVideoControls) { elVideoControls = elVideoHolder.querySelector('.cb-enh-vcontrols-block'); } let elOnlineTime; if(elVideoControls) { elVideoControls.insertAdjacentHTML('beforeend', '<div id="cb-enh-duration"><span></span></div>'); elOnlineTime = elVideoHolder.querySelector('#cb-enh-duration > span'); } gVideoDurationCounters[username] = [ onlineCounterStart, secondsOnline, null, null, elOnlineTime, elOnlineRowValue ]; if(!gIntvUpdateVideoTime) { gIntvUpdateVideoTime = setIntervalRunFirst(updateOnlineTime, 500); } } function destroyVideoDurationCounter(username) { if(!gVideoDurationCounters[username]) { return; } const elOnlineTime = gVideoDurationCounters[username][4]; if(elOnlineTime && elOnlineTime.parentElement) { elOnlineTime.parentElement.remove(); } delete gVideoDurationCounters[username]; if(Object.keys(gVideoDurationCounters).length === 0) { gIntvUpdateVideoTime = clearIntervalEx(gIntvUpdateVideoTime); } } function destroyVideoDurationCounters() { for(const username of Object.keys(gVideoDurationCounters)) { destroyVideoDurationCounter(username); } gIntvUpdateVideoTime = clearIntervalEx(gIntvUpdateVideoTime); gVideoDurationCounters = {}; } function autoAcceptRules() { gIntvAcceptRules = clearIntervalEx(gIntvAcceptRules); gIntvAcceptRules = setInterval(() => { const elAcceptBtn = document.querySelector('.acceptRulesButton'); if(elAcceptBtn && getSetting('auto-chat-rules')) { gIntvAcceptRules = clearIntervalEx(gIntvAcceptRules); elAcceptBtn.click(); } }, 250); } function updateBioRows() { const elAboutVal = document.querySelector('div[data-testid="bio-tab-about-me-value"]'); if(elAboutVal) { toggleElement(elAboutVal.parentElement.parentElement, !isElementVisuallyEmpty(elAboutVal)); } const elWishlistVal = document.querySelector('div[data-testid="bio-tab-wish-list-value"]'); if(elWishlistVal) { toggleElement(elWishlistVal.parentElement.parentElement, !isElementVisuallyEmpty(elWishlistVal)); } } let gIsNavigatingToRandom = false; if(isRandomPage()) { if(!canBeDynamicallyNavigatedFrom()) { addStyle(`.content_body {display: none !important;}`); } if(document.visibilityState !== 'visible') { gIsNavigatingToRandom = true; goToRandomRoom(false, false, window.location.pathname); } } const STYLE_FULL_VIDEO = `div[data-testid="full-video-nav-options"] {display: none !important;}#VideoPanel {border-width: 0 !important;}video {will-change: transform;}#join-overlay,.roomStatusNotifier__moreRoomsLink {display: none !important;}@media (max-width: 380px) {.draggableCanvasWindow {display: none !important;}}`; function sendIframeEvent(elIframe, event, data = null) { elIframe.contentWindow.postMessage({ 'type': 'cb-enh-multi-iframe', 'event': event, 'data': data }, location.origin); } function initFullVideo() { if(window.self === window.top) { return; } if(!window.parent) { return; } if(window.parent.location.origin !== window.location.origin) { return; } if(isFirefox() && window.frameElement?.classList.contains('cb-enh-multi-video-iframe')) { function normalizeMultiIframeMouseupTarget() { window.addEventListener('mouseup', event => { if(!event.isTrusted || event.target !== document || !document.body) { return; } Object.defineProperty(event, 'target', { value: document.body, configurable: true }); }, true); } if(document.documentElement) { runFunction(normalizeMultiIframeMouseupTarget); } else { gFunctionsToRun.push([normalizeMultiIframeMouseupTarget, []]); } } function navigateToRoom(username) { window.dispatchEvent(new PopStateEvent('popstate', { state: { type: 'room', room: username } })); } let navigateTo = null; const params = new URLSearchParams(window?.location?.search); if(params.get('b') === 'multicam') { navigateTo = params.get('n'); } addStyle(STYLE_FULL_VIDEO); enableDarkMode(); function sendEvent(event, data = null) { window.parent.postMessage({ 'type': 'cb-enh-multi-iframe', 'event': event, 'data': data }, location.origin); } let lastVideoEl = null; gIntvPatchVideo = clearIntervalEx(gIntvPatchVideo); gIntvPatchVideo = setIntervalRunFirst(() => { const elVideo = document.querySelector('video[data-testid="video"]'); if(!elVideo) { return; } if(elVideo === lastVideoEl) { return; } lastVideoEl = elVideo; sendEvent('video-found'); }, 100); function setChatVisible(visible) { const elChat = document.getElementById('draggableCanvasChatWindow'); toggleElementVisibility(elChat, visible); } function updateSubject(subject) { sendEvent('update-subject', { 'subject': subject }) } function updateStatus(status) { sendEvent('update-status', { 'status': status }) } waitForElement('#draggableCanvasChatWindow', () => { if(navigateTo) { navigateToRoom(navigateTo); } setChatVisible(false); enableDarkMode(); }); document.addEventListener('click', e => { if(e.target.closest('#chat-close-btn')) { setChatVisible(false); e.preventDefault(); e.stopImmediatePropagation(); return; } }, true); window.addEventListener('message', e => { if(e.origin !== location.origin) { return; } if(e.data.type !== 'cb-enh-multi-iframe') { return; } if(e.data.event === 'set-volume') { const elVideo = document.querySelector('video[data-testid="video"]'); if(elVideo) { elVideo.volume = e.data.data.volume; elVideo.muted = e.data.data.muted; } return; } if(e.data.event === 'toggle-chat') { setChatVisible(null); return; } if(e.data.event === 'toggle-tip') { document.getElementById('menu-tip')?.click(); return; } if(e.data.event === 'toggle-users') { document.querySelector('div[data-testid="users-list"]')?.click(); return; } if(e.data.event === 'toggle-pip') { const elVideo = document.querySelector('video[data-testid="video"]'); if(elVideo) { if(document.pictureInPictureElement === elVideo) { exitVideoPiP(); return; } elVideo.requestPictureInPicture(); } return; } if(e.data.event === 'update-quality') { if(typeof unsafeWindow.hlsInstance !== 'undefined' && unsafeWindow.hlsInstance && unsafeWindow.hlsInstance.levels && unsafeWindow.hlsInstance.levels.length !== 0) { multiUpdateVideoQuality(unsafeWindow.hlsInstance); } return; } }); const OriginalWebSocket = unsafeWindow.WebSocket; unsafeWindow.WebSocket = class extends OriginalWebSocket { constructor(url, protocols) { super(url, protocols); this.addEventListener('message', e => { let data; try { data = JSON.parse(e.data); } catch(e) { return; } if(data.action === 15) { if(!('messages' in data)) { return; } for(const v of data.messages) { if(v.action !== 0) { continue; } if(v.encoding !== 'json') { continue; } if(!('data' in v)) { continue; } let msgData; try { msgData = JSON.parse(v.data); } catch(e) { continue; } if('_topic' in msgData && msgData._topic === 'RoomTitleChangeTopic' && 'title' in msgData) { updateSubject(msgData.title); continue; } if('_topic' in msgData && msgData._topic === 'RoomStatusTopic' && 'status' in msgData) { updateStatus(msgData.status) continue; } } } }); } }; let lastHLSObj = null; gIntvPatchHLS = clearIntervalEx(gIntvPatchHLS); gIntvPatchHLS = setIntervalRunFirst(() => { if(typeof unsafeWindow.hlsInstance !== 'undefined' && lastHLSObj !== unsafeWindow.hlsInstance && unsafeWindow.hlsInstance && unsafeWindow.hlsInstance.levels && unsafeWindow.hlsInstance.levels.length !== 0) { lastHLSObj = unsafeWindow.hlsInstance; updateFakeVisibility(); const preserveManualLevelOnError = getSetting('error-quality') === false; unsafeWindow.__cnh__fnc__.patchHLS(preserveManualLevelOnError); multiUpdateVideoQuality(unsafeWindow.hlsInstance); } }, 100); autoAcceptRules(); } if(gIsFullVideoPage) { initFullVideo(); } function applyVisibilityStatePatches() { let windowObject = window; if(typeof unsafeWindow !== 'undefined') { windowObject = unsafeWindow; } if(typeof windowObject.__cnh__1 === 'undefined') { windowObject.__cnh__1 = {} } windowObject.__cnh__1.getVisibilityState = () => { return document.visibilityState; } const visibilityStateDesc = Object.getOwnPropertyDescriptor( Document.prototype, 'visibilityState' ); if(visibilityStateDesc && visibilityStateDesc.get) { windowObject.__cnh__1.getVisibilityState = () => { return visibilityStateDesc.get.call(document); } function visibilityStateRead() { return windowObject.__cnh__2.gFakeVisibility ? 'visible' : windowObject.__cnh__1.getVisibilityState(); } Object.defineProperty(document, 'visibilityState', { get: visibilityStateRead }); Object.defineProperty(document, 'webkitVisibilityState', { get: visibilityStateRead }); function hiddenStateRead() { return windowObject.__cnh__2.gFakeVisibility ? false : windowObject.__cnh__1.getVisibilityState() !== 'visible'; } Object.defineProperty(document, 'hidden', { get: hiddenStateRead }); Object.defineProperty(document, 'webkitHidden', { get: hiddenStateRead }); Object.defineProperty(document, 'msHidden', { get: hiddenStateRead }); const _hasFocus = windowObject.document.hasFocus; windowObject.document.hasFocus = () => { return windowObject.__cnh__2.gFakeVisibility ? true : _hasFocus.call(windowObject.document); }; } } tryRunFunction(applyVisibilityStatePatches); function applyOtherWindowPatches() { let windowObject = window; if(typeof unsafeWindow !== 'undefined') { windowObject = unsafeWindow; } const pageDocument = windowObject.document; const titleDesc = Object.getOwnPropertyDescriptor(windowObject.Document.prototype, 'title'); if(!titleDesc || !titleDesc.get || !titleDesc.set) { return; } let titleLocked = false; let lockedTitle = ''; let websiteTitle = titleDesc.get.call(pageDocument); const titleObserver = new windowObject.MutationObserver(() => { observeTitleChanges(); restoreLockedTitle(); }); function observeTitleChanges() { titleObserver.disconnect(); if(!titleLocked) { return; } titleObserver.observe(pageDocument, { childList: true }); if(pageDocument.documentElement) { titleObserver.observe(pageDocument.documentElement, { childList: true }); } if(pageDocument.head) { titleObserver.observe(pageDocument.head, { childList: true, subtree: true, characterData: true }); } } function restoreLockedTitle() { if(!titleLocked) { return; } const currentTitle = titleDesc.get.call(pageDocument); if(currentTitle !== lockedTitle) { websiteTitle = currentTitle; titleDesc.set.call(pageDocument, lockedTitle); titleObserver.takeRecords(); } } Object.defineProperty(pageDocument, 'title', { configurable: true, enumerable: titleDesc.enumerable, get() { return titleDesc.get.call(this); }, set(value) { if(titleLocked) { restoreLockedTitle(); websiteTitle = String(value); return; } titleDesc.set.call(this, value); websiteTitle = titleDesc.get.call(this); } }); windowObject.__cnh__fnc__.setPageTitle = (title = null) => { restoreLockedTitle(); if(title === null) { if(titleLocked) { titleLocked = false; titleObserver.disconnect(); titleDesc.set.call(pageDocument, websiteTitle); } return; } if(!titleLocked) { websiteTitle = titleDesc.get.call(pageDocument); } titleDesc.set.call(pageDocument, title); lockedTitle = titleDesc.get.call(pageDocument); titleLocked = true; observeTitleChanges(); }; } tryRunFunction(applyOtherWindowPatches); function capitalize(str) { return str.replace(/(^|[\s_])(\w)/g, (m, sep, ch) => sep + ch.toUpperCase()); } function setPageTitle(title = null) { gPageTitle = title; unsafeWindow.__cnh__fnc__.setPageTitle(gRecording && title !== null ? '🔴 ' + title : title); } if(!gIsFullVideoPage) { updatePageTitle(); } function patchHLS(preserveManualLevelOnError) { let windowObject = window; if(typeof unsafeWindow !== 'undefined') { windowObject = unsafeWindow; } if(typeof windowObject.hlsInstance === 'undefined') { return; } windowObject.hlsInstance.config.preserveManualLevelOnError = preserveManualLevelOnError; if(windowObject.hlsInstance.__cnh__patched) { return; } windowObject.hlsInstance.__cnh__patched = true; const proto = Object.getPrototypeOf(windowObject.hlsInstance); const prop = 'nextLevel'; const desc = Object.getOwnPropertyDescriptor(proto, prop); if(desc && typeof desc.set === 'function') { Object.defineProperty(windowObject.hlsInstance, prop, { get: desc.get.bind(windowObject.hlsInstance), set(value) { if(value === 0 && windowObject.__cnh__2.gFakeVisibility && windowObject.__cnh__1.getVisibilityState() === 'hidden') { return; } return desc.set.call(windowObject.hlsInstance, value); }, }); } } tryAddFunction(patchHLS); function destroyBelowVideoButtons() { gIntvAddVideoControlsBtn = clearIntervalEx(gIntvAddVideoControlsBtn); document.getElementById('cb-enh-below-video-controls')?.remove(); gVideoControlsModalShown = false; } function initVideoControlAnimationHints() { const timers = new WeakMap(); const controlSelector = '#cb-enh-pip, .cb-enh-vcontrols-btn, .cb-enh-seekbar, #full-screen-icon, #theater-mode-icon'; const toMilliseconds = value => parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000); function hintControl(control) { const targets = [...control.querySelectorAll('.video-controls-tooltip, .cb-enh-video-controls-tooltip, .cb-enh-video-controls-seekbar-tooltip')] .map(element => ({element: element, properties: ['opacity']})); if(control.matches('#cb-enh-pip')) { for(const element of control.querySelectorAll('svg')) { targets.push({element: element, properties: ['transform', 'opacity']}); } } if(control.matches('.cb-enh-vcontrols-btn-fs')) { for(const element of control.querySelectorAll('img')) { targets.push({element: element, properties: ['transform', 'opacity']}); } } if(control.matches('#full-screen-icon, #theater-mode-icon')) { for(const element of [control, ...control.querySelectorAll('img, svg')]) { targets.push({element: element, properties: ['transform', 'opacity']}); } } if(control.matches('.cb-enh-seekbar')) { const thumb = control.querySelector('.cb-enh-seekbar__thumb'); if(thumb) { targets.push({element: thumb, properties: ['transform']}); } } let duration = 0; for(const target of targets) { const style = getComputedStyle(target.element); const properties = style.transitionProperty.split(',').map(value => value.trim()); const durations = style.transitionDuration.split(',').map(toMilliseconds); const delays = style.transitionDelay.split(',').map(toMilliseconds); for(let i = 0; i < properties.length; i++) { if(properties[i] === 'all' || target.properties.includes(properties[i])) { duration = Math.max(duration, durations[i % durations.length] + delays[i % delays.length]); } } } if(duration <= 0) { return; } clearTimeout(timers.get(control)); control.classList.add('cb-enh-control-animating'); timers.set(control, setTimeout(() => { control.classList.remove('cb-enh-control-animating'); timers.delete(control); }, duration + 25)); } function onControlHover(event) { const control = event.target.closest?.(controlSelector); if(control && !control.contains(event.relatedTarget)) { hintControl(control); } } function onSeekbarFocus(event) { const control = event.target.closest?.('.cb-enh-seekbar'); if(control && !control.contains(event.relatedTarget)) { hintControl(control); } } document.addEventListener('pointerover', onControlHover); document.addEventListener('pointerout', onControlHover); document.addEventListener('focusin', onSeekbarFocus); document.addEventListener('focusout', onSeekbarFocus); } function initVideoControlsModal() { if(document.getElementById('cb-enh-video-controls-modal')) { return; } const videoControlsModal = document.createElement('div'); videoControlsModal.id = 'cb-enh-video-controls-modal'; videoControlsModal.className = 'cb-enh-tab-bar-modal'; const arrowDown = document.createElement('div'); arrowDown.className = 'cb-enh-arrow cb-enh-arrow-down'; videoControlsModal.appendChild(arrowDown); const contentSpan = document.createElement('span'); contentSpan.id = 'cb-enh-video-controls-modal-content'; contentSpan.innerHTML = videoControlsContentHTML; videoControlsModal.appendChild(contentSpan); document.body.prepend(videoControlsModal); const elKey = document.getElementById('cb-enh-video-controls-zoom-key'); elKey.innerText = isMac() ? '⌘' : 'Ctrl'; } function initBelowVideoButtons() { function intvFunc() { const elSatisfactionScore = document.getElementById('satisfactionScore') if(!elSatisfactionScore) { return; } const elTabBar = document.querySelector('.tabBar'); if(!elTabBar) { return; } gIntvAddVideoControlsBtn = clearIntervalEx(gIntvAddVideoControlsBtn); if(document.getElementById('cb-enh-below-video-controls')) { return; } if(gHbx) { const elPercentScore = elSatisfactionScore.querySelector('span[data-testid="percent-score"]'); if(elPercentScore && isElementHidden(elPercentScore)) { hideElement(elSatisfactionScore); } } const isOnline = !isCurrentRoomOffline(); const elTooltip = document.createElement('div'); elTooltip.className = 'cb-enh-tooltip'; elTooltip.style.zIndex = 20; const elTooltipText = document.createElement('span'); elTooltipText.className = 'ce-loc'; elTooltipText.innerText = 'Hidden cams doesn\'t appear on the cam lists. You can manage hidden cams in the Chaturbate Enhancer settings.'; elTooltipText.setAttribute('data-ce-loc', 'set_blocklist_info_tooltip'); elTooltip.appendChild(elTooltipText); const elTooltipArrow = document.createElement('div'); elTooltipArrow.className = 'cb-enh-arrow cb-enh-arrow-up'; elTooltip.appendChild(elTooltipArrow); elTabBar.prepend(elTooltip); const elEnhControls = document.createElement('span'); elEnhControls.id = 'cb-enh-below-video-controls'; let saveRewindBtn = null; if(isOnline) { const controlsBtn = document.createElement("div"); controlsBtn.id = "cb-enh-video-controls-btn"; controlsBtn.className = "cb-enh-video-bar-btn noselect"; controlsBtn.innerHTML = '<span class="ce-loc" data-ce-loc="vid_controls">Video Controls</span>'; elEnhControls.append(controlsBtn); const recordBtn = document.createElement('div'); recordBtn.id = 'cb-enh-video-controls-record'; recordBtn.className = 'cb-enh-video-bar-btn noselect'; recordBtn.setAttribute('aria-describedby', 'cb-enh-recording-tooltip'); recordBtn.innerHTML = '<span class="ce-loc" data-ce-loc="start_rec">Start recording</span><div id="cb-enh-recording-tooltip" class="cb-enh-tooltip" role="tooltip"><span>0:00:00</span><div class="cb-enh-arrow cb-enh-arrow-up"></div></div>'; elEnhControls.append(recordBtn); saveRewindBtn = document.createElement('div'); saveRewindBtn.id = 'cb-enh-video-controls-save-rewind'; saveRewindBtn.className = 'cb-enh-video-bar-btn noselect'; saveRewindBtn.innerHTML = '<span class="ce-loc" data-ce-loc="save_rewind">Save rewind</span>'; if(getSetting('rewind') === false) { saveRewindBtn.style.display = 'none'; } elEnhControls.append(saveRewindBtn); const screenshotBtn = document.createElement("div"); screenshotBtn.id = "cb-enh-video-controls-screenshot"; screenshotBtn.className = "cb-enh-video-bar-btn noselect"; screenshotBtn.innerHTML = '<span class="ce-loc" data-ce-loc="screenshot">Screenshot</span>'; elEnhControls.append(screenshotBtn); if(!document.getElementById('cb-enh-theater-video-controls-btn')) { const elTheaterControlsHolder = document.querySelector('.theater-video-controls > .theater-overlay'); if(elTheaterControlsHolder) { elTheaterControlsHolder.insertAdjacentHTML('beforeend', `<div id="cb-enh-theater-video-controls-btn" class="hover-btn drop-shadow-container">` + SVG_VIDEO_CONTROLS + `<span><p class="ce-loc" data-ce-loc="vid_controls">Video Controls</p></span></div>`); } } } const elBtnMulti = document.createElement('div'); elBtnMulti.className = 'cb-enh-video-bar-btn noselect'; elBtnMulti.innerHTML = '<span class="ce-loc" data-ce-loc="multi_add">Add to multi cam viewer</span>'; elBtnMulti.addEventListener('click', (e) => { multiAddRoomToList(gCurrentBroadcaster); }); elEnhControls.append(elBtnMulti); let isRoomHidden = blocklistLoadData()['list'].includes(gCurrentBroadcaster); if(!gHbx) { const elBtnHide = document.createElement('div'); elBtnHide.className = 'ce-loc cb-enh-video-bar-btn cb-enh-video-bar-btn-hide noselect'; function updateTooltip() { elBtnHide.innerText = !isRoomHidden ? getLocale('btn_hide_room') : getLocale('btn_unhide_room'); elBtnHide.classList.toggle('cb-enh-active', isRoomHidden); localizeNextTick(); } updateTooltip(); function showTooltip(elBtn) { showElement(elTooltip); const btnRect = elBtn.getBoundingClientRect(); const modalRect = elTooltip.getBoundingClientRect(); const arrowRect = elTooltipArrow.getBoundingClientRect(); const modalLeft = btnRect.left - elTooltip.offsetWidth / 2 + elBtn.offsetWidth / 2 - 4; const modalTop = elBtn.offsetHeight + elTooltipArrow.offsetHeight + 6; elTooltip.style.left = `${modalLeft}px`; elTooltip.style.top = `${modalTop}px`; const left = elTooltip.offsetWidth/2 - elTooltipArrow.offsetWidth/2; const top = -arrowRect.height; elTooltipArrow.style.left = `${left}px`; elTooltipArrow.style.top = `${top}px`; } elBtnHide.addEventListener('click', () => { !isRoomHidden ? blocklistAdd(gCurrentBroadcaster) : blocklistRemove(gCurrentBroadcaster); isRoomHidden = !isRoomHidden; blocklistSave(); updateTooltip(); showTooltip(elBtnHide); }); elBtnHide.addEventListener('mouseover', () => { elTooltipText.className = 'ce-loc'; elTooltipText.innerText = 'Hidden cams doesn\'t appear on the cam lists. You can manage hidden cams in the Chaturbate Enhancer settings.'; elTooltipText.setAttribute('data-ce-loc', 'set_blocklist_info_tooltip'); elTooltip.style.textAlign = 'unset'; elTooltip.style.width = '260px'; showTooltip(elBtnHide); }); elBtnHide.addEventListener('mouseout', () => { hideElement(elTooltip); }); if(saveRewindBtn) { saveRewindBtn.addEventListener('mouseover', () => { const broadcasterName = getCurrentBroadcasterName(); if(broadcasterName && gRewindObjs[broadcasterName]) { const info = rewindGetInfoForTooltip(gRewindObjs[broadcasterName]); const sizeMB = Math.ceil( info.size / 1024 / 1024 ) const duration = formatTime(Math.floor(info.duration)); let infoText = getLocale('save_rewind_t') + `: ${duration} (${sizeMB} MB).`; if(info.sinceLastSave) { infoText += '\n' + getLocale('save_rewind_since_last_save'); } elTooltipText.classList.remove('ce-loc'); elTooltipText.innerText = infoText; elTooltip.style.textAlign = 'center'; elTooltip.style.width = 'unset'; showTooltip(saveRewindBtn); } }); saveRewindBtn.addEventListener('mouseout', () => { hideElement(elTooltip); }); saveRewindBtn.addEventListener('click', () => { hideElement(elTooltip); }); } elEnhControls.append(elBtnHide); } document.getElementById('satisfactionScore')?.insertAdjacentElement('afterend', elEnhControls); updateRecordingUI(); localizeNextTick(); } intvFunc(); gIntvAddVideoControlsBtn = setInterval(intvFunc, 100); } function getClosestVideoHolder(el) { if(el.closest('#cb-enh-video-controls-modal')) { return gVideoControlsCurrentVideoHolder; } return el.closest('.cb-enh-multi-cam, #TheaterModeRoomContents'); } function initBelowVideoButtonsEvents() { document.addEventListener('click', (e) => { let modalPositionType = 0; let el = e.target.closest('#cb-enh-video-controls-btn'); const elVideoHolder = getClosestVideoHolder(e.target); if(!el) { modalPositionType = 1; el = e.target.closest('#cb-enh-theater-video-controls-btn'); if(!el) { return; } } e.preventDefault(); e.stopPropagation(); if(gVideoControlsModalShown && gVideoControlsCurrentVideoHolder === elVideoHolder) { setVideoControlsVisible(false); return; } let elAppendModalTo = null; const isMultiCam = elVideoHolder?.classList.contains('cb-enh-multi-cam'); if(isMultiCam) { elAppendModalTo = document.fullscreenElement || document.getElementById('cb-enh-multi-content'); } else if(modalPositionType === 0) { elAppendModalTo = document.querySelector('.tabBar'); } else { elAppendModalTo = e.target.closest('.theater-video-controls'); } const elModal = document.getElementById('cb-enh-video-controls-modal'); elModal.classList.toggle('cb-enh-multi-video-controls-modal', isMultiCam); elAppendModalTo.append(elModal); setVideoControlsVisible(true, elVideoHolder); document.getElementById('cb-enh-video-controls-modal-show-logo').checked = !getSetting('hide-vid-logo'); document.getElementById('cb-enh-video-controls-modal-duration').checked = getSetting('duration') !== false; document.getElementById('cb-enh-video-controls-modal-rewind').checked = getSetting('rewind') !== false; const elBtn = el; const btnRect = elBtn.getBoundingClientRect(); const modalRect = elModal.getBoundingClientRect(); if(isMultiCam) { const viewportWidth = document.documentElement.clientWidth; const viewportHeight = document.documentElement.clientHeight; const modalLeft = Math.max(8, Math.min(btnRect.left + btnRect.width / 2 - modalRect.width / 2, viewportWidth - modalRect.width - 8)); const preferredTop = btnRect.top >= modalRect.height + 8 ? btnRect.top - modalRect.height - 5 : btnRect.bottom + 5; const modalTop = Math.max(8, Math.min(preferredTop, viewportHeight - modalRect.height - 8)); elModal.style.left = `${modalLeft}px`; elModal.style.top = `${modalTop}px`; return; } let modalLeft = btnRect.left - elModal.offsetWidth / 2 + elBtn.offsetWidth / 2 - 6; if(gIsInMultiView) { const parentRect = elBtn.closest('.theater-video-controls').getBoundingClientRect(); modalLeft = btnRect.left - parentRect.left - elModal.offsetWidth / 2 + elBtn.offsetWidth / 2; } let modalTop = -modalRect.height; if(modalPositionType === 0) { modalTop -= btnRect.height/2; } elModal.style.left = `${modalLeft}px`; elModal.style.top = `${modalTop}px`; const elArrow = document.querySelector('#cb-enh-video-controls-modal .cb-enh-arrow'); const arrowRect = elArrow.getBoundingClientRect(); const left = modalRect.width/2 - elArrow.offsetWidth/2; const top = modalRect.height - 1; elArrow.style.left = `${left}px`; elArrow.style.top = `${top}px`; }); window.addEventListener('click', (e) => { if(gVideoControlsModalShown && !e.target.closest('#cb-enh-video-controls-modal')) { setVideoControlsVisible(false); } }); document.addEventListener('input', (e) => { const el = e.target; switch(el.id) { case 'cb-enh-video-controls-modal-brightness': gVideoControlsCurrentVideoHolder._cnh_videoFilters[0] = [el.value, `brightness(${el.value}%)`]; break; case 'cb-enh-video-controls-modal-contrast': gVideoControlsCurrentVideoHolder._cnh_videoFilters[1] = [el.value, `contrast(${el.value}%)`]; break; case 'cb-enh-video-controls-modal-saturation': gVideoControlsCurrentVideoHolder._cnh_videoFilters[2] = [el.value, `saturate(${el.value}%)`]; break; case 'cb-enh-video-controls-modal-sepia': gVideoControlsCurrentVideoHolder._cnh_videoFilters[3] = [el.value, `sepia(${el.value}%)`]; break; case 'cb-enh-video-controls-modal-hue': gVideoControlsCurrentVideoHolder._cnh_videoFilters[4] = [el.value, `hue-rotate(${el.value}deg)`]; break; case 'cb-enh-video-controls-modal-blur': gVideoControlsCurrentVideoHolder._cnh_videoFilters[5] = [el.value, `blur(${el.value}px)`]; break; case 'cb-enh-video-controls-modal-volume-boost': if(el.value > 1 && hasUserInteracted()) { const elVideo = getVideoHolderVideo(gVideoControlsCurrentVideoHolder); if(elVideo) { elVideo.volume = 1; elVideo.muted = false; if(isMultiViewPage) { multiSetIframeVolume(gVideoControlsCurrentVideoHolder, false, 1); } } } gVideoControlsCurrentVideoHolder._cnh_videoAudioBost.boost = el.value; updateVideoAudioBoost(gVideoControlsCurrentVideoHolder); break; case 'cb-enh-video-controls-modal-volume-voice-boost': gVideoControlsCurrentVideoHolder._cnh_videoAudioBost.voiceBoost = el.value; updateVideoAudioBoost(gVideoControlsCurrentVideoHolder); break; default: return; } updateVideoFilters(gVideoControlsCurrentVideoHolder); }); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-video-controls-modal-reset'); if(!el) { return; } const elVideoHolder = getClosestVideoHolder(e.target); if(!elVideoHolder) { return; } resetVideoFilters(elVideoHolder); resetFreezoom(elVideoHolder); resetVideoAudioBoost(elVideoHolder); document.getElementById('cb-enh-video-controls-modal-mirror-vid').checked = false; document.getElementById('cb-enh-video-controls-modal-invert-vid').checked = false; document.getElementById('cb-enh-video-controls-modal-rotate-left').checked = false; document.getElementById('cb-enh-video-controls-modal-rotate-right').checked = false; document.querySelectorAll('.cb-enh-vid-control-slider').forEach(function(el) { el.value = el.getAttribute('data-default'); }); }); document.addEventListener('click', (e) => { let el = e.target.closest('#cb-enh-video-controls-save-rewind'); if(el) { e.preventDefault(); e.stopPropagation(); const broadcasterName = getCurrentBroadcasterName(); if(broadcasterName && gRewindObjs[broadcasterName]) { rewindDownload(gRewindObjs[broadcasterName]); } return; } el = e.target.closest('#cb-enh-video-controls-screenshot'); if(el) { e.preventDefault(); e.stopPropagation(); captureScreenshot(true); return; } }); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-video-controls-record'); if(!el) { return; } e.preventDefault(); e.stopPropagation(); if(!gRecording) { startRecording(true); } else { stopRecording(true); } }); } function setVideoControlsVisible(visible, elVideoHolder = null) { if(elVideoHolder) { elVideoHolder._cnh_videoFilters ??= []; elVideoHolder._cnh_videoTransformStyle ??= { mirrored: false, inverted: false, rotation: null }; elVideoHolder._cnh_videoAudioBost ??= { boost: 1, voiceBoost: 1 }; let i = -1; for(const v of ['brightness', 'contrast', 'saturation', 'sepia', 'hue', 'blur']) { i++; const el = document.getElementById('cb-enh-video-controls-modal-' + v); if(!el) { continue; } let value; if(typeof elVideoHolder._cnh_videoFilters[i] === 'undefined') { value = el.getAttribute('data-default'); } else { value = elVideoHolder._cnh_videoFilters[i][0]; } el.value = value; } document.getElementById('cb-enh-video-controls-modal-mirror-vid').checked = elVideoHolder._cnh_videoTransformStyle.mirrored; document.getElementById('cb-enh-video-controls-modal-invert-vid').checked = elVideoHolder._cnh_videoTransformStyle.inverted; document.getElementById('cb-enh-video-controls-modal-rotate-left').checked = elVideoHolder._cnh_videoTransformStyle.rotation === false; document.getElementById('cb-enh-video-controls-modal-rotate-right').checked = elVideoHolder._cnh_videoTransformStyle.rotation === true; } queryToggleElement('#cb-enh-video-controls-modal', visible); gVideoControlsModalShown = visible; gVideoControlsCurrentVideoHolder = elVideoHolder; localizeNextTick(); } function initSupportInfo() { gIntvWaitBuyBox = setInterval(() => { const elBox = document.querySelector('div[data-paction="CurrentShowBuyBox"]')?.parentElement; if(!elBox) { return; } gIntvWaitBuyBox = clearIntervalEx(gIntvWaitBuyBox); if(document.getElementById(gtrrft('an'))) { return; } const userType = getUserType(); const msg = getSupportMessage(userType); if(msg === null) { return; } const bBoxHtml = `<div id="`+gtrrft('an')+`" class="noselect"><div class="`+gtrrft('ao')+`"><div class="`+gtrrft('ann')+`" data-utype="${userType}">${msg}</div></div></div>`; elBox.insertAdjacentHTML('beforeend', bBoxHtml); gIntvWaitBuyBox2 = setIntervalRunFirst(function() { const elVideoPanel = document.getElementById('VideoPanel'); if(!elVideoPanel) { return; } const elAccInfo = document.getElementById(gtrrft('an')); if(!elAccInfo) { return; } const elAccInfoInner = document.querySelector('.'+gtrrft('ann')); const panelWidth = elVideoPanel.getBoundingClientRect().width; queryToggleElement('.'+gtrrft('anst'), panelWidth >= 800); if(panelWidth >= 270 + 230 + 700 + 10) { elAccInfo.style.fontSize = '17px'; elAccInfo.style.width = '700px'; elAccInfoInner.style.paddingTop = '4px'; elAccInfoInner.style.paddingBottom = '4px'; showElement(elAccInfo); return; } elAccInfoInner.style.paddingTop = '0px'; elAccInfoInner.style.paddingBottom = '0px'; const parent = elAccInfo.parentElement; const w1 = parent.clientWidth; const chw1 = parent.children[0]?.getBoundingClientRect().width; const chw2 = parent.children[1]?.getBoundingClientRect().width; const w2 = w1 - chw1 - chw2; if(w2 <= 30) { hideElement(elAccInfo); return; } elAccInfo.style.width = (w2 - 10) + 'px'; elAccInfo.style.fontSize = '15px'; showElement(elAccInfo); }, 500); }, 50); } function initSupportInfoEvents() { document.addEventListener('click', (e) => { const el = e.target.closest('.'+gtrrft('ann')); if(!el) { return; } loutrreg(gCurrentBroadcaster); }); } function isLogged() { return unsafeWindow.tsInstance && unsafeWindow.tsInstance.context && typeof unsafeWindow.tsInstance.context.loggedInUser !== 'undefined'; } function getUserType() { let userType = 0; if(isLogged()) { userType = 2; const gaq = document.getElementById('gaq').innerHTML; if(gaq.search(atob(rev(acre8))) !== -1 && gaq.search(atob(rev(acre1 + acre2))) === -1) { userType = 1; } } return userType; } function getSupportMessage(userType) { if(userType === 0) { return `<span class="ce-loc" data-ce-loc="please_support">Support</span> Chaturbate Enhancer <span class="ce-loc" data-ce-loc="ac_msg_dev">development by</span> <u class="ce-loc" data-ce-loc="ac_msg_0">creating free Chaturbate account</u>.<span class="ce-loc" data-ce-loc="thanks">Thank you</span> ❤️`; } else if(userType === 1) { return `<span class="ce-loc" data-ce-loc="please_support">Support</span> Chaturbate Enhancer <span class="ce-loc" data-ce-loc="ac_msg_dev">development by</span> <u class="ce-loc" data-ce-loc="ac_msg_1">creating new Chaturbate account</u>. <span class="ce-loc" data-ce-loc="thanks">Thank you</span> ❤️<div class="`+gtrrft('anst')+`"><span class="ce-loc" data-ce-loc="ac_msg_m_0">If you create new account we will get small commission of every token spend by you. It cost you nothing but it will significantly help further development of Chaturbate Enhancer. This message will disappear.<span></div>`; } return null; } function loutrreg() { setSetting('reg-redir', 1); setSetting('reg-redir-room', gCurrentBroadcaster); if(isLogged()) { document.querySelector('a[href="/auth/logout/"]')?.click(); document.querySelector('.modalAlert .dialog .accept')?.click(); } else { regRedir(gCurrentBroadcaster); } } function regRedir(room) { let rurl = '/accounts/register/'; if(room) { rurl = '/' + room + '/?join_overlay=1&disable_sound=1'; } setSetting('reg-redir', 2); setSetting('reg-redir-room', room); window.location.href = rurl; } function getCurrentVideo() { return document.querySelector('.videoPlayerDiv video'); } let gRewindEnabled = getSetting('rewind') !== false; let gRewindObjs = {}; const SVG_ICON_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" width="28px" height="28px" viewBox="0 -960 960 960" fill="#e3e3e3"><path d="M588-252q-11 0-19.5-8.5T560-280v-400q0-11 8.5-19.5T588-708h78q11 0 19.5 8.5T694-680v400q0 11-8.5 19.5T666-252h-78Zm-294 0q-11 0-19.5-8.5T266-280v-400q0-11 8.5-19.5T294-708h78q11 0 19.5 8.5T400-680v400q0 11-8.5 19.5T372-252h-78Z"/></svg>'; const SVG_ICON_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" width="28px" height="28px" viewBox="0 -960 960 960" fill="#e3e3e3"><path d="M340-302.23v-355.54q0-15.84 10.85-26 10.84-10.15 25.31-10.15 4.61 0 9.53 1.31 4.93 1.3 9.54 3.92l279.84 178.15q8.24 5.62 12.35 13.46 4.12 7.85 4.12 17.08 0 9.23-4.12 17.08-4.11 7.84-12.35 13.46L395.23-271.31q-4.61 2.62-9.54 3.92-4.92 1.31-9.53 1.31-14.47 0-25.31-10.15-10.85-10.16-10.85-26Z"/></svg>'; const HTML_REWIND_UI = `<div class='cb-enh-seekbar' style='--cb-enh-seekbar-progress: 0%; --cb-enh-seekbar-buffer: 0%;'><div class='cb-enh-seekbar__track'><div class='cb-enh-seekbar__buffer'></div><div class='cb-enh-seekbar__progress'></div><div class='cb-enh-seekbar__thumb'></div></div><div class="no-drop-shadow cb-enh-video-controls-seekbar-tooltip"><canvas></canvas><span></span></div></div>`; function rewindSetEnabled(enable) { queryToggleElement('#cb-enh-video-controls-save-rewind', getSetting('rewind') !== false); enable = enable || gRecording; if(gRewindEnabled === enable) { return; } gRewindEnabled = enable; if(enable) { rewindInit0(); } else { rewindDestroyAll(); } } function rewindInit0() { const broadcasterName = getCurrentBroadcasterName(); const elVideoHolder = document.getElementById('TheaterModeRoomContents'); const hlsInstance = unsafeWindow.hlsInstance; if(broadcasterName && elVideoHolder && hlsInstance) { return rewindInit(broadcasterName, elVideoHolder, hlsInstance); } return null; } function rewindInit(username, elVideoHolder, hlsInstance) { if(!gRewindEnabled) { return; } if(gRewindObjs[username]) { return; } const elVideo = getVideoHolderVideo(elVideoHolder); if(!elVideo) { return; } let isRewinding = false; let isPointerdownOnSeekbar = false; const uiElements = []; const template = document.createElement('template'); template.innerHTML = HTML_REWIND_UI.trim(); const elSeekbar = template.content.firstElementChild; const elSeekbarTooltip = elSeekbar.querySelector('.cb-enh-video-controls-seekbar-tooltip'); const elSeekbarTooltipCanvas = elSeekbarTooltip.querySelector('canvas'); elVideoHolder.querySelector('.theater-video-controls').append(elSeekbar); const rewind = HlsRewind.create(updatePlaybackUI); rewind.init(elVideoHolder, hlsInstance, elVideo); function updatePauseButton() { const elIconImg = elPauseBtn.querySelector('svg'); const elTooltip = elPauseBtn.querySelector('.cb-enh-video-controls-tooltip'); elTooltip.classList.remove('ce-loc'); if(rewind.isPaused()) { elIconImg.outerHTML = SVG_ICON_PLAY; elTooltip.innerText = getLocale('play'); } else { elIconImg.outerHTML = SVG_ICON_PAUSE; elTooltip.innerText = getLocale('pause'); } } function updatePlaybackUI() { const rewindTo = rewind.getRewindTo() || 0; const percent = rewindTo / rewind.getMaxDuration(); elSeekbar.style.setProperty('--cb-enh-seekbar-progress', `${percent * 100}%`); if(gVideoDurationCounters[username]) { gVideoDurationCounters[username][2] = rewindTo; gVideoDurationCounters[username][3] = rewind.getPauseTime(); updateOnlineTime(); } elVideoHolder.classList.toggle('cb-enh-rewinded', rewind.isRewinded()); updateVideoUI(elVideoHolder); updatePauseButton(); } const intvUpdate = setInterval(() => { const percent = rewind.getDuration() / rewind.getMaxDuration(); elSeekbar.style.setProperty('--cb-enh-seekbar-buffer', String(percent * 100) + '%'); const pauseTime = rewind.getPauseTime(); if(pauseTime !== null) { const rewindTo = (rewind.getRewindTo() || 0) + Date.now() / 1000 - pauseTime; const percent = rewindTo / rewind.getMaxDuration(); elSeekbar.style.setProperty('--cb-enh-seekbar-progress', `${percent * 100}%`); } }, 1000); async function doRewind(rewindTo) { if(isRewinding) { return; } isRewinding = true; rewindTo = Math.min(rewindTo, rewind.getDuration()); try { if(rewindTo < 5) { await rewind.live(); } else { await rewind.rewind(rewindTo); } updatePlaybackUI(); } catch(e) { isRewinding = false; } isRewinding = false; } elSeekbar.addEventListener('pointerup', () => { isPointerdownOnSeekbar = false; }); elSeekbar.addEventListener('pointerdown', async e => { isPointerdownOnSeekbar = true; if(isRewinding) { return; } const track = elSeekbar.querySelector('.cb-enh-seekbar__track'); const rect = track.getBoundingClientRect(); let percent = Math.max( 0, Math.min(1, (rect.right - e.clientX) / rect.width) ); let rewindTo = percent * rewind.getMaxDuration(); await doRewind(rewindTo); }); let screenshotPreviewTime; function updateSeekbarTooltipPosition(percent) { const rect = elSeekbar.getBoundingClientRect(); const halfWidth = elSeekbarTooltip.getBoundingClientRect().width / 2; const minLeft = halfWidth - rect.left; const maxLeft = document.documentElement.clientWidth - halfWidth - rect.left; elSeekbarTooltip.style.left = `clamp(${minLeft}px, ${(1 - percent) * 100}%, ${maxLeft}px)`; } async function rewindOnSeekbarPointerMove(e) { const rect = elSeekbar.getBoundingClientRect(); const percent = Math.max( 0, Math.min(1, (rect.right - e.clientX) / rect.width) ); let rewindTo = percent * rewind.getMaxDuration(); const rewindDuration = rewind.getDuration(); const isUnseekable = rewindTo > rewindDuration; elSeekbarTooltip.classList.toggle('cb-enh-unseekable', isUnseekable); if(rewindTo < 5) { rewindTo = 0; } rewindTo = Math.floor(rewindTo); let txt; if(rewindTo === 0) { txt = getLocale('live'); } else { txt = '-' + formatTime(rewindTo); } elSeekbarTooltip.querySelector('span').innerText = txt; updateSeekbarTooltipPosition(percent); let hasScreenshot = false; if(!isUnseekable) { const screenshot = rewind.getScreenshot(rewindTo); if(screenshot) { screenshotPreviewTime = screenshot.time; const bitmap = await createImageBitmap(screenshot.blob); if(screenshotPreviewTime !== screenshot.time) { bitmap.close(); return; } const context = elSeekbarTooltipCanvas.getContext('2d'); context.clearRect( 0, 0, elSeekbarTooltipCanvas.width, elSeekbarTooltipCanvas.height ); context.drawImage( bitmap, 0, 0, elSeekbarTooltipCanvas.width, elSeekbarTooltipCanvas.height ); bitmap.close(); hasScreenshot = true; } else { screenshotPreviewTime = null; } } elSeekbarTooltip.classList.toggle('cb-enh-video-controls-seekbar-tooltip-preview', hasScreenshot); updateSeekbarTooltipPosition(percent); if(isPointerdownOnSeekbar && !isRewinding) { await doRewind(rewindTo); } } elSeekbar.addEventListener('pointermove', rewindOnSeekbarPointerMove); async function rewindOnDurationClick(e) { if(!e.target.closest('#cb-enh-duration')) { return; } e.stopImmediatePropagation(); e.preventDefault(); await doRewind(0); } document.addEventListener('click', rewindOnDurationClick); const elVolumeBtn = elVideoHolder.querySelector('div[data-testid="volume-button"]'); const elPauseBtnTemplate = document.createElement('template'); elPauseBtnTemplate.innerHTML = HTML_BOTTOM_VIDEO_CONTROLS_PAUSE_BTN; const elPauseBtn = elPauseBtnTemplate.content.firstElementChild; elVolumeBtn.parentNode.insertBefore(elPauseBtn, elVolumeBtn); const elVolumeBtnCloneTemplate = document.createElement('template'); elVolumeBtnCloneTemplate.innerHTML = HTML_BOTTOM_VIDEO_CONTROLS_VOLUME_BTN; const elVolumeBtnClone = elVolumeBtnCloneTemplate.content.firstElementChild; elVolumeBtn.after(elVolumeBtnClone); const elVolumeSlider = elVideoHolder.querySelector('div[data-testid="volume-slider"]'); const elVolumeSliderCloneTemplate = document.createElement('template'); elVolumeSliderCloneTemplate.innerHTML = HTML_BOTTOM_VIDEO_CONTROLS_VOLUME_SLIDER; const elVolumeSliderClone = elVolumeSliderCloneTemplate.content.firstElementChild; elVolumeSlider.after(elVolumeSliderClone); elVolumeSliderClone.addEventListener('wheel', volumeWheelHandler); elPauseBtn.addEventListener('click', async (e) => { await rewind.playPause(); updatePlaybackUI(); }); uiElements.push(elSeekbar); uiElements.push(elPauseBtn); uiElements.push(elVolumeBtnClone); uiElements.push(elVolumeSliderClone); const rewindObj = { username: username, hls: hlsInstance, elVideoHolder: elVideoHolder, elSeekbar: elSeekbar, rewind: rewind, intvUpdate: intvUpdate, rewindOnDurationClick: rewindOnDurationClick, uiElements: uiElements } gRewindObjs[username] = rewindObj; return rewindObj; } function rewindDestroy(rewindObj) { if(!rewindObj || gRewindObjs[rewindObj.username] !== rewindObj) { return; } if(gRecordingSession?.rewindObj === rewindObj) { stopRecording(false); if(gRewindObjs[rewindObj.username] !== rewindObj) { return; } } rewindObj.rewind.destroy(); clearIntervalEx(rewindObj.intvUpdate); document.removeEventListener('click', rewindObj.rewindOnDurationClick); rewindObj.uiElements.forEach(el => { el.remove(); }); if(gVideoDurationCounters[rewindObj.username]) { gVideoDurationCounters[rewindObj.username][2] = null; updateOnlineTime(); } if(rewindObj.elVideoHolder) { rewindObj.elVideoHolder.classList.remove('cb-enh-rewinded'); rewindObj.elVideoHolder.querySelectorAll('.cb-enh-vcontrols-btn').forEach(el => { el.remove(); }); } delete gRewindObjs[rewindObj.username]; } function rewindDownload(rewindObj) { rewindObj.rewind.dump(rewindObj.username).catch(error => { console.error('Failed to save rewind', error); }); } function rewindGetInfoForTooltip(rewindObj) { return rewindObj.rewind.getDownloadInfo(); } function rewindDestroyAll() { for(const v of Object.values(gRewindObjs)) { rewindDestroy(v); } } function rewindUpdateAll() { const maxDuration = rewindGetMaxRewindTime(); for(const v of Object.values(gRewindObjs)) { if(v.rewind) { const duration = gRecordingSession?.rewindObj === v ? 60 * 10 : maxDuration; const previousDuration = v.rewind.getMaxDuration(); const pauseTime = v.rewind.getPauseTime(); const position = (v.rewind.getRewindTo() || 0) + (pauseTime === null ? 0 : Date.now() / 1000 - pauseTime); v.rewind.setMaxDuration(duration); if(duration < previousDuration && position > duration) { v.rewind.live().catch(error => console.error('Failed to restore live playback', error)); } } } } function rewindGetMaxRewindTime() { const option = getSetting('rewind-time') || 0; const levels = [ 60 * 2, 60 * 5, 60 * 10 ]; return levels[option]; } function copyTransformStyle(elCopyTo, elCopyFrom) { elCopyTo.style.transform = elCopyFrom.style.transform; elCopyTo.style.filter = elCopyFrom.style.filter; } function hlsFragmentStartsWithSync(data, init) { try { function toBytes(value) { return value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength); } function boxes(bytes, start = 0, end = bytes.byteLength) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const result = []; for(let offset = start; offset + 8 <= end;) { let size = view.getUint32(offset); let headerSize = 8; if(size === 1) { headerSize = 16; if(offset + headerSize > end) { return []; } const extendedSize = view.getBigUint64(offset + 8); if(extendedSize > BigInt(Number.MAX_SAFE_INTEGER)) { return []; } size = Number(extendedSize); } else if(size === 0) { size = end - offset; } if(size < headerSize || offset + size > end) { return []; } result.push({ type: String.fromCharCode(...bytes.subarray(offset + 4, offset + 8)), start: offset + headerSize, end: offset + size }); offset += size; } return result; } const bytes = toBytes(data); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const moof = boxes(bytes).find(box => box.type === 'moof'); const traf = moof && boxes(bytes, moof.start, moof.end).find(box => box.type === 'traf'); const children = traf ? boxes(bytes, traf.start, traf.end) : []; const tfhd = children.find(box => box.type === 'tfhd'); const trun = children.find(box => box.type === 'trun'); if(!tfhd || !trun || trun.start + 8 > trun.end || tfhd.start + 8 > tfhd.end || view.getUint32(trun.start + 4) === 0) { return false; } const runFlags = view.getUint32(trun.start) & 0xffffff; let offset = trun.start + 8; if(runFlags & 0x000001) { offset += 4; } let sampleFlags = null; if(runFlags & 0x000004) { if(offset + 4 > trun.end) { return false; } sampleFlags = view.getUint32(offset); } else if(runFlags & 0x000400) { if(runFlags & 0x000100) { offset += 4; } if(runFlags & 0x000200) { offset += 4; } if(offset + 4 > trun.end) { return false; } sampleFlags = view.getUint32(offset); } const headerFlags = view.getUint32(tfhd.start) & 0xffffff; if(sampleFlags === null && (headerFlags & 0x000020)) { offset = tfhd.start + 8; if(headerFlags & 0x000001) { offset += 8; } for(const flag of [0x000002, 0x000008, 0x000010]) { if(headerFlags & flag) { offset += 4; } } if(offset + 4 > tfhd.end) { return false; } sampleFlags = view.getUint32(offset); } if(sampleFlags === null && init) { const initBytes = toBytes(init); const initView = new DataView(initBytes.buffer, initBytes.byteOffset, initBytes.byteLength); const moov = boxes(initBytes).find(box => box.type === 'moov'); const mvex = moov && boxes(initBytes, moov.start, moov.end).find(box => box.type === 'mvex'); const trackId = view.getUint32(tfhd.start + 4); const trex = mvex && boxes(initBytes, mvex.start, mvex.end).find(box => box.type === 'trex' && box.start + 24 <= box.end && initView.getUint32(box.start + 4) === trackId ); if(trex) { sampleFlags = initView.getUint32(trex.start + 20); } } const dependency = sampleFlags === null ? 3 : (sampleFlags >>> 24) & 3; return sampleFlags !== null && (sampleFlags & 0x00010000) === 0 && dependency !== 1 && dependency !== 3; } catch(error) { return false; } } const HlsRewind = (() => { function create(onLive = null) { let maxDuration = rewindGetMaxRewindTime(); const BUFFER_AHEAD_SECONDS = 45; const BUFFER_BEHIND_SECONDS = 20; const INITIAL_BUFFER_AHEAD_SECONDS = 8; const MIN_BUFFER_AHEAD_SECONDS = 2; const MAX_QUOTA_RETRIES = 4; let isLive = true; let isRewinding = false; let rewindedTo = null; let elVideoHolder = null; let hls = null; let liveVideo = null; let rewindVideo = null; let bIsPaused = false; let pauseTime = null; let pausedLiveMuted = null; let previewCanvas = null; let videoInit = null; let audioInit = null; let initGeneration = 0; let initPromise = null; let recording = null; let videoContinuity = null; let videoParts = []; let audioParts = []; const MAX_INCOMPLETE_APPEND_BYTES = 32 * 1024 * 1024; const MAX_AUDIO_SPLIT_DELAY_SECONDS = 30; const RECORDING_INIT_WAIT_MS = 5000; let incompleteAppends = {video: null, audio: null}; let mediaClocks = {video: null, audio: null}; let mediaAnchors = {video: null, audio: null}; let nextCaptureOrder = 0; let downloadState = { video: null, audio: null, busy: false }; let videoLevel = -1; let audioTrackIndex = -1; let totalBytes = 0; let pump = null; let activeSession = 0; const SCREENSHOT_INTERVAL = 5; let screenshotCanvas = null; let screenshotContext = null; let screenshots = []; let screenshotBytes = 0; let lastScreenshotMediaTime = null; let screenshotCaptureBusy = false; function liveVideoOnVolumeChange(e) { if(!isLive || bIsPaused) { liveVideo.muted = true; e.preventDefault(); e.stopImmediatePropagation(); } } function liveVideoOnPlay() { if(!isLive || bIsPaused) { liveVideo.muted = true; } } function liveVideoOnTimeUpdate() { maybeCaptureScreenshot(); } function init(elVideoHolder_arg, hlsInstance, elVideo) { destroy(); elVideoHolder = elVideoHolder_arg; hls = hlsInstance; liveVideo = elVideo; videoLevel = hls.currentLevel; audioTrackIndex = hls.audioTrack; refreshInitSegments(); createVideo(); hls.on( Hls.Events.BUFFER_APPENDING, onBufferAppending ); hls.on( Hls.Events.LEVEL_SWITCHED, onLevelSwitched ); liveVideo.addEventListener( 'volumechange', liveVideoOnVolumeChange ); liveVideo.addEventListener( 'play', liveVideoOnPlay ); liveVideo.addEventListener( 'timeupdate', liveVideoOnTimeUpdate ); } function setMaxDuration(seconds) { maxDuration = Number(seconds); prune(); } function getMaxDuration() { return maxDuration; } function getSize() { return totalBytes; } function refreshInitSegments() { initPromise = captureInitSegments().catch(error => { console.error('Failed to load rewind initialization data', error); return {video: null, audio: null}; }); } async function captureInitSegments() { const generation = initGeneration; const level = hls?.levels[videoLevel]; const audio = hls?.audioTracks[hls.audioTrack]; const video = level?.details?.fragments?.[0]?.initSegment; const audioSegment = audio?.details?.fragments?.[0]?.initSegment; const [videoData, audioData] = await Promise.all([ video?.data ? copyData(video.data) : video?._url ? xmlhttpRequestArrayBuffer(video._url) : null, audioSegment?.data ? copyData(audioSegment.data) : audioSegment?._url ? xmlhttpRequestArrayBuffer(audioSegment._url) : null ]); if(generation === initGeneration) { videoInit ||= videoData; audioInit ||= audioData; if(videoInit && !mediaClocks.video) { captureMediaClock('video', videoInit); } if(audioInit && !mediaClocks.audio) { captureMediaClock('audio', audioInit); } if(recording?.chunk.generation === generation) { recording.chunk.videoInit ||= videoInit; recording.chunk.audioInit ||= audioInit; processRecording(); } } return {video: videoData, audio: audioData}; } function createVideo() { rewindVideo = document.createElement('video'); rewindVideo.classList.add( 'cb-enh-rewind-video' ); rewindVideo.controls = false; rewindVideo.playsInline = true; rewindVideo.muted = true; rewindVideo.style.cssText = `position: absolute;inset: 0;width: 100%;height: 100%;visibility: hidden;`; rewindVideo.addEventListener( 'ended', onRewindEnded ); previewCanvas = document.createElement('canvas'); previewCanvas.classList.add( 'cb-enh-rewind-video-canvas' ); liveVideo.parentElement?.prepend( previewCanvas ); liveVideo.parentElement?.appendChild( rewindVideo ); setFreezoomEnabled( elVideoHolder, true, rewindVideo ); setFreezoomEnabled( elVideoHolder, true, previewCanvas ); createScreenshotCanvas(); } function createScreenshotCanvas() { screenshotCanvas = document.createElement('canvas'); screenshotCanvas.width = 400; screenshotCanvas.height = 225; screenshotContext = screenshotCanvas.getContext('2d'); } async function maybeCaptureScreenshot() { if( screenshotCaptureBusy || !liveVideo || liveVideo.paused || liveVideo.readyState < 2 || !screenshotCanvas || !screenshotContext ) { return; } const mediaTime = liveVideo.currentTime; if(!Number.isFinite(mediaTime)) { return; } if( lastScreenshotMediaTime !== null && mediaTime - lastScreenshotMediaTime < SCREENSHOT_INTERVAL ) { return; } screenshotCaptureBusy = true; try { screenshotContext.drawImage( liveVideo, 0, 0, screenshotCanvas.width, screenshotCanvas.height ); const blob = await canvasToBlob( screenshotCanvas, 'image/jpeg', 0.80 ); if(!blob) { return; } screenshots.push({ time: mediaTime, blob }); screenshotBytes += blob.size; lastScreenshotMediaTime = mediaTime; pruneScreenshots(); } finally { screenshotCaptureBusy = false; } } function canvasToBlob( canvas, type, quality ) { return new Promise( resolve => { canvas.toBlob( resolve, type, quality ); } ); } function pruneScreenshots() { if(!screenshots.length) { return; } const newest = videoParts[ videoParts.length - 1 ]; if(!newest) { return; } const cacheEnd = newest.start + newest.duration; const cutoff = cacheEnd - maxDuration; while(screenshots.length) { const screenshot = screenshots[0]; if( screenshot.time >= cutoff ) { break; } screenshotBytes -= screenshot.blob.size; screenshots.shift(); } } function getScreenshot(secondsAgo) { if(!screenshots.length) { return null; } const newest = videoParts[ videoParts.length - 1 ]; if(!newest) { return null; } const cacheEnd = newest.start + newest.duration; const target = cacheEnd - Number(secondsAgo); let low = 0; let high = screenshots.length - 1; while(low <= high) { const middle = (low + high) >> 1; const time = screenshots[ middle ].time; if(time < target) { low = middle + 1; } else if(time > target) { high = middle - 1; } else { return screenshots[ middle ]; } } const before = screenshots[ Math.max( 0, high ) ]; const after = screenshots[ Math.min( screenshots.length - 1, low ) ]; if(!before) { return after; } if(!after) { return before; } return ( Math.abs( before.time - target ) <= Math.abs( after.time - target ) ? before : after ); } function onRewindEnded() { live(); } function mp4BoxList(bytes, start = 0, end = bytes.byteLength) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const boxes = []; let offset = start; while(offset + 8 <= end) { let size = view.getUint32(offset); let headerSize = 8; if(size === 1) { if(offset + 16 > end) { return {boxes, incomplete: true}; } const largeSize = view.getBigUint64(offset + 8); if(largeSize > BigInt(Number.MAX_SAFE_INTEGER)) { return {boxes, invalid: true}; } size = Number(largeSize); headerSize = 16; } else if(size === 0) { size = end - offset; } if(size < headerSize) { return {boxes, invalid: true}; } if(size > end - offset) { return {boxes, incomplete: true}; } const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8)); boxes.push({type, offset, end: offset + size, dataStart: offset + headerSize}); offset += size; } return {boxes, incomplete: offset !== end}; } function mp4Child(bytes, parent, type) { if(!parent) { return null; } return mp4BoxList(bytes, parent.dataStart, parent.end).boxes.find(box => box.type === type) || null; } function captureMediaClock(type, data) { const bytes = new Uint8Array(data); const boxes = mp4BoxList(bytes).boxes; const moov = boxes.find(box => box.type === 'moov'); if(!moov) { return; } const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const tracks = mp4BoxList(bytes, moov.dataStart, moov.end).boxes.filter(box => box.type === 'trak'); for(const trak of tracks) { const mdia = mp4Child(bytes, trak, 'mdia'); const hdlr = mp4Child(bytes, mdia, 'hdlr'); const mdhd = mp4Child(bytes, mdia, 'mdhd'); if(!hdlr || !mdhd || hdlr.dataStart + 12 > hdlr.end) { continue; } const handler = String.fromCharCode(...bytes.subarray(hdlr.dataStart + 8, hdlr.dataStart + 12)); if(handler !== (type === 'video' ? 'vide' : 'soun')) { continue; } const at = mdhd.dataStart + (bytes[mdhd.dataStart] === 1 ? 20 : 12); if(at + 4 > mdhd.end) { continue; } const timescale = view.getUint32(at); if(!timescale) { continue; } let defaultDuration = null; const mvex = mp4Child(bytes, moov, 'mvex'); const trex = mp4Child(bytes, mvex, 'trex'); if(trex && trex.dataStart + 16 <= trex.end) { defaultDuration = view.getUint32(trex.dataStart + 8); } mediaClocks[type] = {timescale, defaultDuration}; mediaAnchors[type] = null; return; } } function mp4FragmentTime(bytes, type) { try { const moof = mp4BoxList(bytes).boxes.find(box => box.type === 'moof'); const traf = mp4Child(bytes, moof, 'traf'); const tfdt = mp4Child(bytes, traf, 'tfdt'); const tfhd = mp4Child(bytes, traf, 'tfhd'); if(!tfdt || !tfhd) { return null; } const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if(tfdt.dataStart + (bytes[tfdt.dataStart] === 1 ? 12 : 8) > tfdt.end) { return null; } const decode = bytes[tfdt.dataStart] === 1 ? view.getBigUint64(tfdt.dataStart + 4) : BigInt(view.getUint32(tfdt.dataStart + 4)); const headerFlags = view.getUint32(tfhd.dataStart) & 0xffffff; let at = tfhd.dataStart + 8; if(headerFlags & 0x000001) { at += 8; } if(headerFlags & 0x000002) { at += 4; } let defaultDuration = mediaClocks[type]?.defaultDuration ?? null; if(headerFlags & 0x000008) { if(at + 4 > tfhd.end) { return {decode, duration: null}; } defaultDuration = view.getUint32(at); } let duration = 0n; const runs = mp4BoxList(bytes, traf.dataStart, traf.end).boxes.filter(box => box.type === 'trun'); for(const run of runs) { if(run.dataStart + 8 > run.end) { return {decode, duration: null}; } const flags = view.getUint32(run.dataStart) & 0xffffff; const count = view.getUint32(run.dataStart + 4); let cursor = run.dataStart + 8 + ((flags & 1) ? 4 : 0) + ((flags & 4) ? 4 : 0); const stride = ((flags & 0x100) ? 4 : 0) + ((flags & 0x200) ? 4 : 0) + ((flags & 0x400) ? 4 : 0) + ((flags & 0x800) ? 4 : 0); if(cursor > run.end || count > Math.floor((run.end - cursor) / Math.max(1, stride))) { return {decode, duration: null}; } if(!(flags & 0x100)) { if(!defaultDuration) { return {decode, duration: null}; } duration += BigInt(defaultDuration) * BigInt(count); continue; } for(let i = 0; i < count; i++, cursor += stride) { duration += BigInt(view.getUint32(cursor)); } } return {decode, duration: duration > 0n ? duration : null}; } catch(error) { console.warn('HlsRewind: skipping malformed media fragment', error); return null; } } function bufferAppendPackets(type, data, meta) { let bytes = new Uint8Array(data); if(!bytes.length) { return []; } const previous = incompleteAppends[type]; if(previous) { if(previous.key === meta.key) { const retriedPrefix = bytes.length >= previous.bytes.length && previous.bytes.every((value, index) => bytes[index] === value); const freshBoxes = mp4BoxList(bytes); const completeRetry = !freshBoxes.invalid && !freshBoxes.incomplete && freshBoxes.boxes.some(box => box.type === 'moof') && freshBoxes.boxes.some(box => box.type === 'mdat'); if(!retriedPrefix && !completeRetry) { const combined = new Uint8Array(previous.bytes.length + bytes.length); combined.set(previous.bytes); combined.set(bytes, previous.bytes.length); bytes = combined; } else { console.warn('HlsRewind: replacing incomplete ' + type + ' append with retransmitted media'); } } else { console.warn('HlsRewind: discarding unfinished ' + type + ' fragment after stream change'); } } incompleteAppends[type] = null; const scanned = mp4BoxList(bytes); if(scanned.invalid) { console.warn('HlsRewind: ignoring invalid ' + type + ' MP4 append'); return []; } let first = 0; let hasMoof = false; const packets = []; for(const box of scanned.boxes) { if(box.type === 'moof') { if(hasMoof) { console.warn('HlsRewind: skipping moof without mdat'); first = box.offset; } hasMoof = true; } else if(box.type === 'mdat') { if(hasMoof) { const fragmentBoxes = scanned.boxes.filter(item => item.offset >= first && item.end <= box.end && !['sidx', 'ssix', 'mfra'].includes(item.type)); const size = fragmentBoxes.reduce((sum, item) => sum + item.end - item.offset, 0); const fragment = new Uint8Array(size); let position = 0; for(const item of fragmentBoxes) { fragment.set(bytes.subarray(item.offset, item.end), position); position += item.end - item.offset; } packets.push(fragment); } else { console.warn('HlsRewind: skipping mdat without moof'); } first = box.end; hasMoof = false; } } if(first < bytes.length) { const remainder = bytes.slice(first); if(remainder.length <= MAX_INCOMPLETE_APPEND_BYTES) { incompleteAppends[type] = {key: meta.key, bytes: remainder}; } else { console.warn('HlsRewind: discarding oversized incomplete ' + type + ' fragment'); } } return packets; } function onBufferAppending(event, data) { try { if(!data?.data || !data.frag || !['video', 'audio'].includes(data.type)) { return; } const buffer = copyData(data.data); if(!buffer.byteLength) { return; } if(data.type === 'video') { const continuityChanged = videoContinuity !== null && data.frag.cc !== videoContinuity; if(data.frag.level !== videoLevel || continuityChanged) { onLevelSwitched(event, { level: data.frag.level, start: data.part?.start ?? data.frag.start, appending: true, continuityChanged }); } videoContinuity = data.frag.cc; } const bytes = new Uint8Array(buffer); const boxType = bytes.length >= 8 ? String.fromCharCode(...bytes.subarray(4, 8)) : null; if(boxType === 'ftyp' || boxType === 'moov') { if(data.type === 'video') { videoInit = buffer; } else { audioInit = buffer; } captureMediaClock(data.type, buffer); incompleteAppends[data.type] = null; if(recording) { recording.chunk.videoInit = videoInit; recording.chunk.audioInit = audioInit; processRecording(); } return; } const meta = { sn: Number(data.frag.sn), part: data.part?.index ?? -1, cc: data.frag.cc, start: data.part?.start ?? data.frag.start, duration: data.part?.duration ?? data.frag.duration }; if(!Number.isFinite(meta.sn) || !Number.isFinite(meta.start) || !Number.isFinite(meta.duration)) { return; } meta.key = `${meta.cc}:${meta.sn}:${meta.part}`; const packets = bufferAppendPackets(data.type, buffer, meta); const list = data.type === 'video' ? videoParts : audioParts; let changed = false; for(const packet of packets) { const timing = mp4FragmentTime(packet, data.type); if(!timing) { console.warn('HlsRewind: ignoring ' + data.type + ' fragment without valid tfdt'); continue; } const clock = mediaClocks[data.type]; let start = meta.start; let duration = meta.duration; if(clock) { let anchor = mediaAnchors[data.type]; if(!anchor || anchor.key !== meta.key) { anchor = {key: meta.key, decode: timing.decode, start: meta.start}; mediaAnchors[data.type] = anchor; } start = anchor.start + Number(timing.decode - anchor.decode) / clock.timescale; if(timing.duration !== null) { duration = Number(timing.duration) / clock.timescale; } } const part = { sn: meta.sn, part: meta.part, cc: meta.cc, key: `${meta.key}:${timing.decode}`, decode: timing.decode, start, duration, data: packet.buffer }; if(!addPart(list, part)) { continue; } changed = true; if(recording) { const chunk = recording.chunk; const selected = data.type === 'video' ? recording.recordVideo : recording.recordAudio; if(selected && (chunk.start === null || part.start + part.duration > chunk.start)) { const parts = data.type === 'video' ? chunk.videoParts : chunk.audioParts; parts.push(part); parts.sort((a, b) => a.start - b.start || a.sn - b.sn || a.part - b.part); } } } if(changed) { processRecording(); prune(); } } catch(error) { console.warn('HlsRewind: ignoring failed media append', error); incompleteAppends[data?.type] = null; } } function addPart(list, part) { if(list.some(item => item.key === part.key)) { return false; } part.captureOrder = ++nextCaptureOrder; list.push(part); totalBytes += part.data.byteLength; list.sort((a, b) => a.start - b.start || a.sn - b.sn || a.part - b.part || (a.captureOrder - b.captureOrder)); return true; } function prune() { if(!videoParts.length) { return; } const newest = videoParts[videoParts.length - 1]; const newestEnd = newest.start + newest.duration; const cutoff = newestEnd - maxDuration; pruneList( videoParts, cutoff ); pruneList( audioParts, cutoff ); pruneScreenshots(); } function pruneList(list, cutoff) { while(list.length) { const item = list[0]; const end = item.start + item.duration; if(end >= cutoff) { break; } totalBytes -= item.data.byteLength; list.shift(); } } function onLevelSwitched(event, data) { if((recording && !data.appending) || (data.level === videoLevel && !data.continuityChanged)) { return; } const session = recording; const sameAudio = session && !data.continuityChanged && audioTrackIndex === hls.audioTrack; const audioCut = Number.isFinite(data.start) ? data.start : -Infinity; const carryAudio = sameAudio ? audioParts.filter(part => session.recordVideo ? part.start + part.duration > audioCut : part.start >= audioCut ) : []; const carryAudioInit = sameAudio ? audioInit : null; if(session) { if(sameAudio && !session.recordVideo) { session.chunk.audioParts = session.chunk.audioParts.filter(part => part.start < audioCut); } queueRecordingChunk(session, session.chunk); } videoLevel = data.level; audioTrackIndex = hls.audioTrack; ++initGeneration; videoContinuity = null; videoParts = []; audioParts = carryAudio; incompleteAppends.video = null; mediaClocks.video = null; mediaAnchors.video = null; if(!sameAudio) { incompleteAppends.audio = null; mediaClocks.audio = null; mediaAnchors.audio = null; } videoInit = null; audioInit = carryAudioInit; downloadState = {video: null, audio: null, busy: false}; totalBytes = carryAudio.reduce((size, part) => size + part.data.byteLength, 0); screenshots = []; screenshotBytes = 0; lastScreenshotMediaTime = null; refreshInitSegments(); if(session && recording === session) { session.chunk = createRecordingChunk(session, -Infinity, false); session.chunk.audioParts = session.recordAudio ? [...carryAudio] : []; } } function isRewinded() { return !isLive; } function getDuration() { if(!videoParts.length) { return 0; } const first = videoParts[0]; const last = videoParts[ videoParts.length - 1 ]; return Math.max( 0, last.start + last.duration - first.start ); } function getData() { return { videoInit, audioInit, videoParts: [ ...videoParts ], audioParts: [ ...audioParts ] }; } async function loadInitSegments(needVideo = true, needAudio = true) { const generation = initGeneration; const level = hls?.levels[videoLevel]; const audio = hls?.audioTracks[hls.audioTrack]; const videoSegment = level?.details?.fragments?.[0]?.initSegment; const audioSegment = audio?.details?.fragments?.[0]?.initSegment; const readSegment = segment => { if(segment?.data) { return Promise.resolve(copyData(segment.data)); } if(segment?._url) { return xmlhttpRequestArrayBuffer(String(segment._url)); } return Promise.resolve(null); }; const [video, audioData] = await Promise.all([ needVideo ? readSegment(videoSegment) : null, needAudio ? readSegment(audioSegment) : null ]); if(generation !== initGeneration) { throw new Error('Rewind quality changed while loading init segments'); } if((needVideo && !video) || (needAudio && !audioData)) { throw new Error('Failed to load required HLS init segments'); } return {video, audio: audioData}; } async function waitForRecordingInit(promise) { if(!promise) { return null; } let timer; try { return await Promise.race([ Promise.resolve(promise).catch(error => { console.warn('HlsRewind: recording init request failed', error); return null; }), new Promise(resolve => { timer = setTimeout(() => resolve(null), RECORDING_INIT_WAIT_MS); }) ]); } finally { clearTimeout(timer); } } function createRecordingChunk(session, requestedStart, includeBuffer) { return { videoParts: includeBuffer && session.recordVideo ? [...videoParts] : [], audioParts: includeBuffer && session.recordAudio ? [...audioParts] : [], videoInit, audioInit, initPromise, generation: initGeneration, recordVideo: session.recordVideo, recordAudio: session.recordAudio, requestedStart, start: null, date: new Date() }; } function prepareRecordingChunk(chunk) { if(chunk.start !== null) { return true; } let index = -1; const parts = chunk.recordVideo ? chunk.videoParts : chunk.audioParts; if(chunk.recordVideo) { for(let i = 0; i < parts.length && parts[i].start <= chunk.requestedStart; i++) { if(hlsFragmentStartsWithSync(parts[i].data, chunk.videoInit)) { index = i; } } if(index === -1) { index = parts.findIndex(part => part.start >= chunk.requestedStart && hlsFragmentStartsWithSync(part.data, chunk.videoInit)); } } else { index = parts.findIndex(part => part.start + part.duration > chunk.requestedStart); } if(index === -1) { return false; } chunk.start = parts[index].start; if(Number.isFinite(chunk.requestedStart)) { chunk.date = new Date(chunk.date.getTime() + (chunk.start - chunk.requestedStart) * 1000); } chunk.videoParts = chunk.videoParts.filter(part => part.start >= chunk.start); chunk.audioParts = chunk.audioParts.filter(part => part.start + part.duration > chunk.start); return true; } function queueRecordingChunk(session, chunk) { if(!chunk.videoParts.length && !chunk.audioParts.length) { return true; } if(session.queuedSaves >= 3 && recording === session) { session.onError(new Error('Recording downloads cannot keep up with the stream')); return false; } const snapshot = {...chunk, videoParts: [...chunk.videoParts], audioParts: [...chunk.audioParts]}; ++session.queuedSaves; session.pending = session.pending.then(async () => { if((snapshot.recordVideo && snapshot.videoParts.length && !snapshot.videoInit) || (snapshot.recordAudio && snapshot.audioParts.length && !snapshot.audioInit)) { const init = await waitForRecordingInit(snapshot.initPromise); snapshot.videoInit ||= init?.video; snapshot.audioInit ||= init?.audio; } snapshot.recordVideo = snapshot.recordVideo && !!snapshot.videoParts.length && !!snapshot.videoInit; snapshot.recordAudio = snapshot.recordAudio && !!snapshot.audioParts.length && !!snapshot.audioInit; if(!snapshot.recordVideo && !snapshot.recordAudio) { console.warn('HlsRewind: no complete media tracks available to save'); return; } let ready = prepareRecordingChunk(snapshot); if(!ready && snapshot.recordVideo && snapshot.recordAudio) { console.warn('HlsRewind: no video keyframe in recording chunk; saving audio only'); snapshot.recordVideo = false; snapshot.start = null; ready = prepareRecordingChunk(snapshot); } if(ready) { await dumpSnapshot(session.fname, null, snapshot); } }).catch(error => { session.onError(error); }).finally(() => { --session.queuedSaves; }); return true; } function processRecording() { const session = recording; if(!session) { return; } const chunk = session.chunk; if(!prepareRecordingChunk(chunk)) { return; } const parts = session.recordVideo ? chunk.videoParts : chunk.audioParts; const boundary = parts.find(part => part.start >= chunk.start + 60 * 10 && (!session.recordVideo || hlsFragmentStartsWithSync(part.data, chunk.videoInit))); if(!boundary) { return; } if(session.recordAudio) { const lastAudio = chunk.audioParts[chunk.audioParts.length - 1]; if(!lastAudio || lastAudio.start + lastAudio.duration < boundary.start) { const newest = parts[parts.length - 1]; const newestEnd = newest.start + newest.duration; if(newestEnd < boundary.start + MAX_AUDIO_SPLIT_DELAY_SECONDS) { return; } console.warn('HlsRewind: audio is missing or delayed; saving available media'); } } const snapshot = { ...chunk, videoParts: chunk.videoParts.filter(part => part.start < boundary.start), audioParts: chunk.audioParts.filter(part => part.start < boundary.start) }; if(!queueRecordingChunk(session, snapshot)) { return; } chunk.videoParts = chunk.videoParts.filter(part => part.start >= boundary.start); chunk.audioParts = chunk.audioParts.filter(part => session.recordVideo ? part.start + part.duration > boundary.start : part.start >= boundary.start ); chunk.date = new Date(chunk.date.getTime() + (boundary.start - chunk.start) * 1000); chunk.start = boundary.start; } function startRecordingSession(fname, options) { if(recording) { throw new Error('Rewind recording is already running'); } const session = { fname, recordAudio: options.recordAudio, recordVideo: options.recordVideo, onError: options.onError, pending: Promise.resolve(), queuedSaves: 0, chunk: null }; const newest = videoParts[videoParts.length - 1]; const start = Number.isFinite(liveVideo.currentTime) ? liveVideo.currentTime : newest ? newest.start + newest.duration : 0; session.chunk = createRecordingChunk(session, start, true); recording = session; processRecording(); } function stopRecordingSession() { const session = recording; if(!session) { return Promise.resolve(); } recording = null; queueRecordingChunk(session, session.chunk); return session.pending; } function getUnsavedParts(parts, lastSavedPart) { if(!lastSavedPart) { return parts; } return parts.filter(part => part.captureOrder !== undefined && lastSavedPart.captureOrder !== undefined ? part.captureOrder > lastSavedPart.captureOrder : part.sn > lastSavedPart.sn || (part.sn === lastSavedPart.sn && part.part > lastSavedPart.part) ); } function getDownloadInfo() { const video = getUnsavedParts(videoParts, downloadState.video); const audio = getUnsavedParts(audioParts, downloadState.audio); const sinceLastSave = downloadState.video !== null || downloadState.audio !== null; const primary = video.length ? video : audio; if(!primary.length) { return {duration: 0, size: 0, sinceLastSave}; } const last = primary[primary.length - 1]; return { duration: Math.max(0, last.start + last.duration - primary[0].start), size: video.reduce((size, part) => size + part.data.byteLength, 0) + audio.reduce((size, part) => size + part.data.byteLength, 0), sinceLastSave }; } async function dump(fname) { const state = downloadState; if(state.busy) { return; } state.busy = true; try { await dumpSnapshot(fname, state); } finally { state.busy = false; } } async function dumpSnapshot(fname, state, snapshot = null) { const snapshotTime = Date.now(); let recordVideo = snapshot?.recordVideo !== false; let recordAudio = snapshot?.recordAudio !== false; let snapshotDate = snapshot?.date ? new Date(snapshot.date) : null; const snapshotInitPromise = snapshot?.initPromise; if(!recordVideo && !recordAudio) { return; } const sourceVideoParts = snapshot ? snapshot.videoParts || [] : getUnsavedParts(videoParts, state?.video); const sourceAudioParts = snapshot ? snapshot.audioParts || [] : getUnsavedParts(audioParts, state?.audio); recordVideo = recordVideo && sourceVideoParts.length > 0; recordAudio = recordAudio && sourceAudioParts.length > 0; if(!recordVideo && !recordAudio) { return; } let snapshotVideoParts = (recordVideo ? sourceVideoParts : []).map(part => ({ sn: part.sn, part: part.part, start: part.start, duration: part.duration, captureOrder: part.captureOrder, data: snapshot ? part.data : copyData(part.data) })); let snapshotAudioParts = (recordAudio ? sourceAudioParts : []).map(part => ({ sn: part.sn, part: part.part, start: part.start, duration: part.duration, captureOrder: part.captureOrder, data: snapshot ? part.data : copyData(part.data) })); let snapshotVideoInit = (recordVideo && (snapshot ? snapshot.videoInit : videoInit)) ? copyData(snapshot ? snapshot.videoInit : videoInit) : null; let snapshotAudioInit = (recordAudio && (snapshot ? snapshot.audioInit : audioInit)) ? copyData(snapshot ? snapshot.audioInit : audioInit) : null; if(!snapshotVideoParts.length && !snapshotAudioParts.length) { return; } const lastVideoPart = snapshotVideoParts[snapshotVideoParts.length - 1]; const lastAudioPart = snapshotAudioParts[snapshotAudioParts.length - 1]; if( (recordVideo && !snapshotVideoInit) || (recordAudio && !snapshotAudioInit) ) { let init = null; if(snapshot) { init = await waitForRecordingInit(snapshotInitPromise); } else { const needVideo = recordVideo && !snapshotVideoInit; const needAudio = recordAudio && !snapshotAudioInit; init = await waitForRecordingInit(loadInitSegments(needVideo, needAudio)); } if(recordVideo && !snapshotVideoInit && init?.video) { snapshotVideoInit = copyData(init.video); } if(recordAudio && !snapshotAudioInit && init?.audio) { snapshotAudioInit = copyData(init.audio); } } recordVideo = recordVideo && !!snapshotVideoInit; recordAudio = recordAudio && !!snapshotAudioInit; if(!recordVideo && !recordAudio) { throw new Error('HLS init segments unavailable for all recorded tracks'); } const CONTAINER_TYPES = new Set([ 'moov', 'trak', 'mdia', 'minf', 'stbl', 'edts', 'dinf', 'mvex', 'moof', 'traf', 'mfra', 'meta' ]); function toUint8(data) { if(data instanceof Uint8Array) { return data; } if(data instanceof ArrayBuffer) { return new Uint8Array(data); } if(ArrayBuffer.isView(data)) { return new Uint8Array( data.buffer, data.byteOffset, data.byteLength ); } throw new TypeError( 'Expected binary data' ); } function readUint32(data, offset) { return new DataView( data.buffer, data.byteOffset, data.byteLength ).getUint32(offset); } function readInt32(data, offset) { return new DataView( data.buffer, data.byteOffset, data.byteLength ).getInt32(offset); } function readUint64(data, offset) { return new DataView( data.buffer, data.byteOffset, data.byteLength ).getBigUint64(offset); } function writeUint32( data, offset, value ) { if( offset < 0 || offset + 4 > data.byteLength ) { throw new RangeError( `writeUint32 outside buffer: offset=${offset}, length=${data.byteLength}` ); } new DataView( data.buffer, data.byteOffset, data.byteLength ).setUint32( offset, value >>> 0 ); } function writeUint64( data, offset, value ) { if( offset < 0 || offset + 8 > data.byteLength ) { throw new RangeError( `writeUint64 outside buffer: offset=${offset}, length=${data.byteLength}` ); } new DataView( data.buffer, data.byteOffset, data.byteLength ).setBigUint64( offset, BigInt(value) ); } function readType(data, offset) { return String.fromCharCode( data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7] ); } function parseBoxes( data, start = 0, end = data.byteLength ) { const bytes = toUint8(data); const boxes = []; let offset = start; while( offset + 8 <= end ) { let size = readUint32( bytes, offset ); let headerSize = 8; if(size === 1) { if( offset + 16 > end ) { throw new Error( 'Invalid extended MP4 box' ); } const extendedSize = readUint64( bytes, offset + 8 ); if( extendedSize > BigInt( Number.MAX_SAFE_INTEGER ) ) { throw new Error( 'MP4 box is too large' ); } size = Number( extendedSize ); headerSize = 16; } else if(size === 0) { size = end - offset; } if( size < headerSize || offset + size > end ) { throw new Error( `Invalid MP4 box size: ${size}` ); } boxes.push({ offset, size, headerSize, type: readType( bytes, offset ), dataStart: offset + headerSize, dataEnd: offset + size }); offset += size; } return boxes; } function findBoxes( data, type, start = 0, end = data.byteLength, results = [] ) { const bytes = toUint8(data); for( const box of parseBoxes( bytes, start, end ) ) { if(box.type === type) { results.push(box); } if( CONTAINER_TYPES.has( box.type ) ) { let childStart = box.dataStart; if(box.type === 'meta') { childStart += 4; } if( childStart < box.dataEnd ) { findBoxes( bytes, type, childStart, box.dataEnd, results ); } } } return results; } function findFirstBox( data, type, start = 0, end = data.byteLength ) { return findBoxes( data, type, start, end )[0] || null; } function directChildren( data, box ) { return parseBoxes( data, box.dataStart, box.dataEnd ); } function concat(parts) { let size = 0; for(const part of parts) { const bytes = toUint8(part); if( size > Number.MAX_SAFE_INTEGER - bytes.byteLength ) { throw new RangeError( 'Output is too large' ); } size += bytes.byteLength; } const result = new Uint8Array(size); let offset = 0; for(const part of parts) { const bytes = toUint8(part); result.set( bytes, offset ); offset += bytes.byteLength; } return result; } function makeBox( type, payload ) { const bytes = toUint8(payload); const size = 8 + bytes.byteLength; if(size > 0xffffffff) { throw new Error( `MP4 box too large: ${type}` ); } const result = new Uint8Array(size); writeUint32( result, 0, size ); result[4] = type.charCodeAt(0); result[5] = type.charCodeAt(1); result[6] = type.charCodeAt(2); result[7] = type.charCodeAt(3); result.set( bytes, 8 ); return result; } function getHandlerType(trak) { const hdlr = findFirstBox( trak, 'hdlr' ); if(!hdlr) { return null; } const bytes = toUint8(trak); return String.fromCharCode( bytes[hdlr.dataStart + 8], bytes[hdlr.dataStart + 9], bytes[hdlr.dataStart + 10], bytes[hdlr.dataStart + 11] ); } function getTrackId(trak) { const bytes = toUint8(trak); const tkhd = findFirstBox( bytes, 'tkhd' ); if(!tkhd) { throw new Error( 'Track has no tkhd' ); } const version = bytes[tkhd.dataStart]; return readUint32( bytes, version === 1 ? tkhd.offset + 28 : tkhd.offset + 20 ); } function getTrackTimescale(trak) { const bytes = toUint8(trak); const mdhd = findFirstBox( bytes, 'mdhd' ); if(!mdhd) { throw new Error( 'Track has no mdhd' ); } const version = bytes[mdhd.dataStart]; return readUint32( bytes, version === 1 ? mdhd.offset + 28 : mdhd.offset + 20 ); } function getTracks(moov) { const moovBox = findFirstBox( moov, 'moov' ); if(!moovBox) { throw new Error( 'Could not find moov' ); } return directChildren( moov, moovBox ) .filter( box => box.type === 'trak' ) .map(box => { const bytes = toUint8(moov).slice( box.offset, box.offset + box.size ); return { bytes, id: getTrackId( bytes ), handler: getHandlerType( bytes ), timescale: getTrackTimescale( bytes ) }; }); } function rewriteTkhdTrackId( trak, trackId ) { const result = toUint8(trak).slice(); const tkhd = findFirstBox( result, 'tkhd' ); if(!tkhd) { throw new Error( 'Track has no tkhd' ); } const version = result[ tkhd.dataStart ]; writeUint32( result, version === 1 ? tkhd.offset + 28 : tkhd.offset + 20, trackId ); return result; } function getTrackTrex( mvex, trackId ) { const mvexBox = findFirstBox( mvex, 'mvex' ); if(!mvexBox) { return null; } for( const child of directChildren( mvex, mvexBox ) ) { if(child.type !== 'trex') { continue; } const bytes = toUint8(mvex).slice( child.offset, child.offset + child.size ); if( readUint32( bytes, 12 ) === trackId ) { return bytes; } } return null; } function rewriteTrexTrackId( trex, trackId ) { const result = toUint8(trex).slice(); writeUint32( result, 12, trackId ); return result; } function getTfhdInfo( data, tfhd ) { const bytes = toUint8(data); const flags = readUint32( bytes, tfhd.dataStart ) & 0xffffff; let offset = tfhd.dataStart + 8; let baseDataOffsetOffset = null; let defaultDurationOffset = null; let defaultDuration = null; let defaultSize = null; let defaultFlags = null; if(flags & 0x000001) { baseDataOffsetOffset = offset; offset += 8; } if(flags & 0x000002) { offset += 4; } if(flags & 0x000008) { defaultDurationOffset = offset; defaultDuration = readUint32( bytes, offset ); offset += 4; } if(flags & 0x000010) { defaultSize = readUint32(bytes, offset); offset += 4; } if(flags & 0x000020) { defaultFlags = readUint32(bytes, offset); } return { flags, baseDataOffsetOffset, defaultDurationOffset, defaultDuration, defaultSize, defaultFlags }; } function getTfdt( data, traf ) { const tfdt = findFirstBox( data, 'tfdt', traf.dataStart, traf.dataEnd ); if(!tfdt) { throw new Error( 'Fragment has no tfdt' ); } const bytes = toUint8(data); return { box: tfdt, value: bytes[ tfdt.dataStart ] === 1 ? readUint64( bytes, tfdt.offset + 12 ) : BigInt( readUint32( bytes, tfdt.offset + 12 ) ) }; } function getTrunInfo( data, trun ) { const bytes = toUint8(data); const version = bytes[ trun.dataStart ]; const flags = readUint32( bytes, trun.dataStart ) & 0xffffff; const sampleCount = readUint32( bytes, trun.dataStart + 4 ); let offset = trun.dataStart + 8; let dataOffsetOffset = null; let firstSampleFlagsOffset = null; if(flags & 0x000001) { dataOffsetOffset = offset; offset += 4; } if(flags & 0x000004) { firstSampleFlagsOffset = offset; offset += 4; } const durationOffsets = []; const sizeOffsets = []; const sampleFlagsOffsets = []; const compositionOffsetOffsets = []; for( let i = 0; i < sampleCount; i++ ) { let durationOffset = null; let sizeOffset = null; let sampleFlagsOffset = null; let compositionOffsetOffset = null; if(flags & 0x000100) { durationOffset = offset; offset += 4; } if(flags & 0x000200) { sizeOffset = offset; offset += 4; } if(flags & 0x000400) { sampleFlagsOffset = offset; offset += 4; } if(flags & 0x000800) { compositionOffsetOffset = offset; offset += 4; } durationOffsets.push( durationOffset ); sizeOffsets.push( sizeOffset ); sampleFlagsOffsets.push( sampleFlagsOffset ); compositionOffsetOffsets.push( compositionOffsetOffset ); } return { version, flags, sampleCount, dataOffsetOffset, firstSampleFlagsOffset, durationOffsets, sizeOffsets, sampleFlagsOffsets, compositionOffsetOffsets }; } function parseFragment( data, trexDefaultDuration ) { const bytes = toUint8(data); const moof = findFirstBox( bytes, 'moof' ); if(!moof) { throw new Error( 'Fragment has no moof' ); } const trafs = directChildren( bytes, moof ).filter( box => box.type === 'traf' ); if(!trafs.length) { throw new Error( 'Fragment has no traf' ); } let decodeStart = null; let decodeEnd = null; let presentationStart = null; let presentationEnd = null; let presentationOffset = null; for(const traf of trafs) { const tfhd = findFirstBox( bytes, 'tfhd', traf.dataStart, traf.dataEnd ); const tfdt = getTfdt( bytes, traf ); if(!tfhd) { throw new Error( 'Fragment has no tfhd' ); } const tfhdInfo = getTfhdInfo( bytes, tfhd ); const defaultDuration = tfhdInfo.defaultDuration ?? trexDefaultDuration; let decodeTime = tfdt.value; const truns = findBoxes( bytes, 'trun', traf.dataStart, traf.dataEnd ); for(const trun of truns) { const info = getTrunInfo( bytes, trun ); for( let i = 0; i < info.sampleCount; i++ ) { const duration = info.durationOffsets[i] !== null ? BigInt( readUint32( bytes, info.durationOffsets[i] ) ) : BigInt( defaultDuration ); if(duration <= 0n) { throw new Error( 'Invalid MP4 sample duration' ); } let cto = 0n; if( info.compositionOffsetOffsets[i] !== null ) { if(info.version === 1) { cto = BigInt( readInt32( bytes, info.compositionOffsetOffsets[i] ) ); } else { cto = BigInt( readUint32( bytes, info.compositionOffsetOffsets[i] ) ); } } const pts = decodeTime + cto; if( decodeStart === null || decodeTime < decodeStart ) { decodeStart = decodeTime; } if( presentationStart === null || pts < presentationStart ) { presentationStart = pts; } const end = pts + duration; if( presentationEnd === null || end > presentationEnd ) { presentationEnd = end; } if( presentationOffset === null || pts - decodeTime < presentationOffset ) { presentationOffset = pts - decodeTime; } decodeTime += duration; } } if( decodeEnd === null || decodeTime > decodeEnd ) { decodeEnd = decodeTime; } } if( decodeStart === null || decodeEnd === null || presentationStart === null || presentationEnd === null || presentationOffset === null ) { throw new Error( 'Could not determine fragment timeline' ); } return { moof, trafs, decodeStart, decodeEnd, presentationStart, presentationEnd, presentationOffset }; } const exportedTracks = []; function trimAudioPart(part, track, trex, presentationOffset, rangeStart, rangeEnd) { const bytes = toUint8(part.data); const topBoxes = parseBoxes(bytes); const moofs = topBoxes.filter(box => box.type === 'moof'); const mdats = topBoxes.filter(box => box.type === 'mdat'); if(moofs.length !== 1 || mdats.length !== 1 || mdats[0].offset < moofs[0].offset) { throw new Error('Unsupported audio fragment layout for recording'); } const moof = moofs[0]; const mdat = mdats[0]; const moofChildren = directChildren(bytes, moof); const trafs = moofChildren.filter(box => box.type === 'traf'); if(trafs.length !== 1) { throw new Error('Recording audio fragment must contain one track'); } const traf = trafs[0]; const children = directChildren(bytes, traf); const tfhd = children.find(box => box.type === 'tfhd'); const tfdt = children.find(box => box.type === 'tfdt'); if(!tfhd || !tfdt) { throw new Error('Audio fragment has no track header or decode time'); } const header = getTfhdInfo(bytes, tfhd); const defaultDuration = header.defaultDuration ?? readUint32(trex, 20); const defaultSize = header.defaultSize ?? readUint32(trex, 24); const defaultFlags = header.defaultFlags ?? readUint32(trex, 28); const baseOffset = header.baseDataOffsetOffset === null ? moof.offset : Number(readUint64(bytes, header.baseDataOffsetOffset)); let decodeTime = getTfdt(bytes, traf).value; let dataOffset = mdat.dataStart; const samples = []; for(const trun of children.filter(box => box.type === 'trun')) { const run = getTrunInfo(bytes, trun); if(run.dataOffsetOffset !== null) { dataOffset = baseOffset + readInt32(bytes, run.dataOffsetOffset); } for(let i = 0; i < run.sampleCount; i++) { const duration = run.durationOffsets[i] === null ? defaultDuration : readUint32(bytes, run.durationOffsets[i]); const size = run.sizeOffsets[i] === null ? defaultSize : readUint32(bytes, run.sizeOffsets[i]); const flags = run.sampleFlagsOffsets[i] !== null ? readUint32(bytes, run.sampleFlagsOffsets[i]) : i === 0 && run.firstSampleFlagsOffset !== null ? readUint32(bytes, run.firstSampleFlagsOffset) : defaultFlags; const compositionOffset = run.compositionOffsetOffsets[i] === null ? 0 : run.version === 1 ? readInt32(bytes, run.compositionOffsetOffsets[i]) : readUint32(bytes, run.compositionOffsetOffsets[i]); if(duration <= 0 || size <= 0 || dataOffset < mdat.dataStart || dataOffset + size > mdat.dataEnd) { throw new Error('Invalid audio sample bounds'); } const time = Number(decodeTime + BigInt(compositionOffset)) / track.timescale + presentationOffset; samples.push({ duration, size, flags, compositionOffset, decodeTime, data: bytes.subarray(dataOffset, dataOffset + size), keep: time >= rangeStart && time < rangeEnd }); decodeTime += BigInt(duration); dataOffset += size; } } const selected = samples.filter(sample => sample.keep); if(!selected.length) { return null; } if(selected.length === samples.length) { return part; } const firstIndex = samples.findIndex(sample => sample.keep); if(samples.slice(firstIndex, firstIndex + selected.length).some(sample => !sample.keep)) { throw new Error('Recording audio samples are not contiguous'); } if(children.some(box => ['saiz', 'saio', 'senc', 'sbgp', 'sgpd', 'subs', 'uuid'].includes(box.type))) { throw new Error('Unsupported audio sample metadata for recording trim'); } const headerPayload = new Uint8Array(header.flags & 0x000002 ? 12 : 8); writeUint32(headerPayload, 0, 0x020000 | (header.flags & 0x000002)); writeUint32(headerPayload, 4, readUint32(bytes, tfhd.dataStart + 4)); if(header.flags & 0x000002) { writeUint32(headerPayload, 8, readUint32(bytes, tfhd.dataStart + 8 + (header.flags & 0x000001 ? 8 : 0))); } const newTfhd = makeBox('tfhd', headerPayload); const extendedTime = bytes[tfdt.dataStart] === 1 || selected[0].decodeTime > 0xffffffffn; const timePayload = new Uint8Array(extendedTime ? 12 : 8); if(extendedTime) { writeUint32(timePayload, 0, 0x01000000); writeUint64(timePayload, 4, selected[0].decodeTime); } else { writeUint32(timePayload, 4, Number(selected[0].decodeTime)); } const newTfdt = makeBox('tfdt', timePayload); const signedOffsets = selected.some(sample => sample.compositionOffset < 0); const runPayload = new Uint8Array(12 + selected.length * 16); writeUint32(runPayload, 0, (signedOffsets ? 0x01000000 : 0) | 0x000f01); writeUint32(runPayload, 4, selected.length); for(let i = 0; i < selected.length; i++) { const sample = selected[i]; if(signedOffsets && sample.compositionOffset > 0x7fffffff) { throw new Error('Audio composition offset overflows signed trun'); } const offset = 12 + i * 16; writeUint32(runPayload, offset, sample.duration); writeUint32(runPayload, offset + 4, sample.size); writeUint32(runPayload, offset + 8, sample.flags); writeUint32(runPayload, offset + 12, sample.compositionOffset); } const newTrun = makeBox('trun', runPayload); const trafPayload = []; let runInserted = false; for(const child of children) { if(child.type === 'tfhd') { trafPayload.push(newTfhd); } else if(child.type === 'tfdt') { trafPayload.push(newTfdt); } else if(child.type === 'trun') { if(!runInserted) { trafPayload.push(newTrun); runInserted = true; } } else { trafPayload.push(bytes.slice(child.offset, child.offset + child.size)); } } const newTraf = makeBox('traf', concat(trafPayload)); const newMoof = makeBox('moof', concat(moofChildren.map(child => child === traf ? newTraf : bytes.slice(child.offset, child.offset + child.size) ))); const newMdat = makeBox('mdat', concat(selected.map(sample => sample.data))); const retainedBoxes = topBoxes.filter(box => !['sidx', 'mfra'].includes(box.type)); const gapSize = retainedBoxes.filter(box => box.offset > moof.offset && box.offset < mdat.offset) .reduce((size, box) => size + box.size, 0); const outputRun = findFirstBox(newMoof, 'trun'); writeUint32(newMoof, outputRun.dataStart + 8, newMoof.byteLength + gapSize + 8); const data = concat(retainedBoxes.map(box => box === moof ? newMoof : box === mdat ? newMdat : bytes.slice(box.offset, box.offset + box.size))); return {...part, data: data.buffer}; } function getSourcePresentationOffset(moov, track) { const elst = findFirstBox(track.bytes, 'elst'); if(!elst) { return 0; } const mvhd = findFirstBox(moov, 'mvhd'); if(!mvhd) { throw new Error('moov has no mvhd'); } const movieTimescale = readUint32(moov, moov[mvhd.dataStart] === 1 ? mvhd.offset + 28 : mvhd.offset + 20); const view = new DataView(track.bytes.buffer, track.bytes.byteOffset, track.bytes.byteLength); const version = track.bytes[elst.dataStart]; const count = readUint32(track.bytes, elst.dataStart + 4); const entrySize = version === 1 ? 20 : 12; let emptyDuration = 0; for(let i = 0, offset = elst.dataStart + 8; i < count; i++, offset += entrySize) { if(offset + entrySize > elst.dataEnd) { throw new Error('Invalid MP4 edit list'); } const duration = version === 1 ? Number(view.getBigUint64(offset)) : view.getUint32(offset); const mediaTime = version === 1 ? view.getBigInt64(offset + 8) : BigInt(view.getInt32(offset + 4)); if(mediaTime === -1n) { emptyDuration += duration / movieTimescale; continue; } const rateOffset = offset + (version === 1 ? 16 : 8); if(view.getInt16(rateOffset) !== 1 || view.getInt16(rateOffset + 2) !== 0) { throw new Error('Unsupported MP4 edit rate'); } return emptyDuration - Number(mediaTime) / track.timescale; } return emptyDuration; } function addExportTrack(type, init, parts) { const initData = toUint8(init); const moovBox = findFirstBox(initData, 'moov'); if(!moovBox) { throw new Error('Could not find MP4 moov'); } const moov = initData.slice(moovBox.offset, moovBox.offset + moovBox.size); const track = getTracks(moov).find(item => item.handler === (type === 'video' ? 'vide' : 'soun')); if(!track) { throw new Error('Could not find ' + type + ' track'); } const moovChildren = directChildren(moov, findFirstBox(moov, 'moov')); const mvex = moovChildren.find(box => box.type === 'mvex'); if(!mvex) { throw new Error('Missing mvex'); } const mvexBytes = moov.slice(mvex.offset, mvex.offset + mvex.size); const trex = getTrackTrex(mvexBytes, track.id); if(!trex) { throw new Error('Could not find trex'); } const presentationOffset = getSourcePresentationOffset(moov, track); if(type === 'video') { const firstSync = parts.findIndex(part => hlsFragmentStartsWithSync(part.data, initData)); if(firstSync < 0) { throw new Error('cache does not contain a video random-access point'); } parts = parts.slice(firstSync); snapshotVideoParts = parts; } else if(recordVideo) { const videoWallStart = snapshotVideoParts[0].start; let firstAudioIndex = parts.findIndex(part => part.start + part.duration > videoWallStart); if(firstAudioIndex < 0) { firstAudioIndex = parts.length - 1; } parts = parts.slice(Math.max(0, firstAudioIndex - 1)); snapshotAudioParts = parts; } if(type === 'audio' && snapshot) { const video = recordVideo ? exportedTracks[0] : null; const start = Number.isFinite(snapshot.start) ? snapshot.start : parts[0].start; const rangeStart = video ? video.sourceStartSeconds : Math.max(start, Number.isFinite(snapshot.requestedStart) ? snapshot.requestedStart : start); const rangeEnd = video ? video.sourceEndSeconds : Infinity; if(!video && snapshotDate) { snapshotDate = new Date(snapshotDate.getTime() + (rangeStart - start) * 1000); } parts = parts.map(part => { try { return trimAudioPart(part, track, trex, presentationOffset, rangeStart, rangeEnd); } catch(error) { console.warn('HlsRewind: skipping invalid recording audio part', error); return null; } }).filter(Boolean); snapshotAudioParts = parts; if(!parts.length) { return; } } const parsed = parts.map(part => ({part, info: parseFragment(part.data, readUint32(trex, 20))})); const sourceStart = parsed.reduce((value, item) => value === null || item.info.presentationStart < value ? item.info.presentationStart : value, null ); const sourceEnd = parsed.reduce((value, item) => value === null || item.info.presentationEnd > value ? item.info.presentationEnd : value, null ); if(sourceStart === null || sourceEnd === null) { throw new Error('Could not determine exported media timeline'); } exportedTracks.push({ type, parts, initData, moov, moovChildren, mvexBytes, trex, track, parsed, presentationOffset, id: exportedTracks.length ? exportedTracks[0].id + 1 : track.id, sourceStartSeconds: Number(sourceStart) / track.timescale + presentationOffset, sourceEndSeconds: Number(sourceEnd) / track.timescale + presentationOffset }); } if(recordVideo) { addExportTrack('video', snapshotVideoInit, snapshotVideoParts); } if(recordAudio) { addExportTrack('audio', snapshotAudioInit, snapshotAudioParts); } if(!exportedTracks.length) { return; } const commonPresentationStartSeconds = Math.min(...exportedTracks.map(entry => entry.sourceStartSeconds)); let timelineShiftSeconds = 0; for(const entry of exportedTracks) { const first = entry.parsed[0].info; entry.targetFirstTfdt = BigInt(Math.max(0, Math.round( (Number(first.presentationStart) / entry.track.timescale + entry.presentationOffset - commonPresentationStartSeconds) * entry.track.timescale ))) - (first.presentationStart - first.decodeStart); if(entry.targetFirstTfdt < 0n) { timelineShiftSeconds = Math.max(timelineShiftSeconds, Number(-entry.targetFirstTfdt) / entry.track.timescale); } } if(timelineShiftSeconds > 0) { for(const entry of exportedTracks) { entry.targetFirstTfdt += BigInt(Math.round(timelineShiftSeconds * entry.track.timescale)); } } for(const entry of exportedTracks) { const first = entry.parsed[0].info; let mediaEnd = first.decodeEnd; for(const item of entry.parsed) { if(item.info.decodeEnd > mediaEnd) { mediaEnd = item.info.decodeEnd; } if(item.info.presentationEnd > mediaEnd) { mediaEnd = item.info.presentationEnd; } } entry.outputEndSeconds = Number(mediaEnd - first.decodeStart + entry.targetFirstTfdt) / entry.track.timescale; } const outputDuration = Math.max(...exportedTracks.map(entry => entry.outputEndSeconds)); const baseTrack = exportedTracks[0]; const mergedMvexPayload = []; for(const child of directChildren(baseTrack.mvexBytes, findFirstBox(baseTrack.mvexBytes, 'mvex'))) { if(child.type !== 'trex') { mergedMvexPayload.push(baseTrack.mvexBytes.slice(child.offset, child.offset + child.size)); } } for(const entry of exportedTracks) { mergedMvexPayload.push(rewriteTrexTrackId(entry.trex, entry.id)); } const mergedMvex = makeBox('mvex', concat(mergedMvexPayload)); const mergedMoovPayload = []; let tracksInserted = false; for(const child of baseTrack.moovChildren) { if(child.type === 'mvex') { mergedMoovPayload.push(mergedMvex); } else if(child.type === 'trak') { if(!tracksInserted) { for(const entry of exportedTracks) { const trak = findFirstBox(entry.track.bytes, 'trak'); const payload = directChildren(entry.track.bytes, trak) .filter(box => box.type !== 'edts') .map(box => entry.track.bytes.slice(box.offset, box.offset + box.size)); mergedMoovPayload.push(rewriteTkhdTrackId(makeBox('trak', concat(payload)), entry.id)); } tracksInserted = true; } } else { mergedMoovPayload.push(baseTrack.moov.slice(child.offset, child.offset + child.size)); } } let mergedMoov = makeBox('moov', concat(mergedMoovPayload)); function rewriteDurations(moov) { const result = toUint8(moov).slice(); const mvhd = findFirstBox( result, 'mvhd' ); if(!mvhd) { throw new Error( 'moov has no mvhd' ); } const mvhdVersion = result[ mvhd.dataStart ]; const movieTimescale = readUint32( result, mvhdVersion === 1 ? mvhd.offset + 28 : mvhd.offset + 20 ); const movieDuration = Math.max( 0, Math.ceil( outputDuration * movieTimescale ) ); const mvhdDurationOffset = mvhdVersion === 1 ? mvhd.offset + 32 : mvhd.offset + 24; writeUint32(result, mvhd.dataEnd - 4, Math.max(...exportedTracks.map(entry => entry.id)) + 1); if(mvhdVersion === 1) { writeUint64( result, mvhdDurationOffset, BigInt( movieDuration ) ); } else { if( movieDuration > 0xffffffff ) { throw new Error( 'mvhd duration overflow' ); } writeUint32( result, mvhdDurationOffset, movieDuration ); } const mehd = findFirstBox( result, 'mehd' ); if(mehd) { const version = result[ mehd.dataStart ]; if(version === 1) { writeUint64( result, mehd.offset + 12, BigInt( movieDuration ) ); } else { if( movieDuration > 0xffffffff ) { throw new Error( 'mehd duration overflow' ); } writeUint32( result, mehd.offset + 12, movieDuration ); } } for( const trak of findBoxes( result, 'trak' ) ) { const trakData = result.slice( trak.offset, trak.offset + trak.size ); const tkhd = findFirstBox( trakData, 'tkhd' ); const mdhd = findFirstBox( trakData, 'mdhd' ); if(!tkhd || !mdhd) { continue; } const trackId = getTrackId( trakData ); const entry = exportedTracks.find(item => item.id === trackId); if(!entry) { continue; } const trackDuration = Math.max(0, entry.outputEndSeconds); const trackTimescale = entry.track.timescale; const mdhdDuration = Math.ceil( trackDuration * trackTimescale ); const tkhdDuration = Math.ceil( trackDuration * movieTimescale ); const tkhdVersion = trakData[ tkhd.dataStart ]; const mdhdVersion = trakData[ mdhd.dataStart ]; const tkhdDurationOffset = tkhdVersion === 1 ? tkhd.offset + 36 : tkhd.offset + 28; const mdhdDurationOffset = mdhdVersion === 1 ? mdhd.offset + 32 : mdhd.offset + 24; if(tkhdVersion === 1) { writeUint64( result, trak.offset + tkhdDurationOffset, BigInt( Math.max( 0, tkhdDuration ) ) ); } else { if( tkhdDuration > 0xffffffff ) { throw new Error( 'tkhd duration overflow' ); } writeUint32( result, trak.offset + tkhdDurationOffset, Math.max( 0, tkhdDuration ) ); } if(mdhdVersion === 1) { writeUint64( result, trak.offset + mdhdDurationOffset, BigInt( Math.max( 0, mdhdDuration ) ) ); } else { if( mdhdDuration > 0xffffffff ) { throw new Error( 'mdhd duration overflow' ); } writeUint32( result, trak.offset + mdhdDurationOffset, Math.max( 0, mdhdDuration ) ); } } return result; } mergedMoov = rewriteDurations( mergedMoov ); const ftypBox = findFirstBox(baseTrack.initData, 'ftyp'); if(!ftypBox) { throw new Error('HLS init has no ftyp'); } const ftyp = baseTrack.initData.slice(ftypBox.offset, ftypBox.offset + ftypBox.size); function rewriteFragment( source, trackId, firstSourceTfdt, targetFirstTfdt, sequenceNumber, outputMoofOffset ) { const result = toUint8(source).slice(); const moof = findFirstBox( result, 'moof' ); if(!moof) { throw new Error( 'Fragment has no moof' ); } const trafs = directChildren( result, moof ).filter( box => box.type === 'traf' ); for( let trafIndex = 0; trafIndex < trafs.length; trafIndex++ ) { const traf = trafs[trafIndex]; const tfhd = findFirstBox( result, 'tfhd', traf.dataStart, traf.dataEnd ); const tfdt = findFirstBox( result, 'tfdt', traf.dataStart, traf.dataEnd ); if(!tfhd || !tfdt) { continue; } writeUint32( result, tfhd.offset + 12, trackId ); const tfhdInfo = getTfhdInfo( result, tfhd ); if( tfhdInfo.baseDataOffsetOffset !== null ) { const oldBase = readUint64( result, tfhdInfo.baseDataOffsetOffset ); const relativeBase = oldBase - BigInt( moof.offset ); const newBase = BigInt( outputMoofOffset ) + relativeBase; if(newBase < 0n) { throw new Error( 'Invalid base-data-offset' ); } writeUint64( result, tfhdInfo.baseDataOffsetOffset, newBase ); } const sourceTraf = trafs[trafIndex]; const sourceTfdt = getTfdt( source, sourceTraf ); const targetTfdt = targetFirstTfdt + ( sourceTfdt.value - firstSourceTfdt ); if(targetTfdt < 0n) { throw new Error( 'Negative target tfdt' ); } const version = result[ tfdt.dataStart ]; if(version === 1) { writeUint64( result, tfdt.offset + 12, targetTfdt ); } else { if( targetTfdt > 0xffffffffn ) { throw new Error( 'tfdt does not fit version 0' ); } writeUint32( result, tfdt.offset + 12, Number( targetTfdt ) ); } } const mfhd = findFirstBox( result, 'mfhd', moof.dataStart, moof.dataEnd ); if(mfhd) { writeUint32( result, mfhd.offset + 12, sequenceNumber ); } return result; } const fragments = []; for(const track of exportedTracks) { for(const entry of track.parsed) { fragments.push({type: track.type, part: entry.part, track}); } } fragments.sort((a, b) => { if( a.part.start !== b.part.start ) { return ( a.part.start - b.part.start ); } if(a.type !== b.type) { return a.type === 'video' ? -1 : 1; } if(a.part.sn !== b.part.sn) { return ( a.part.sn - b.part.sn ); } return ( a.part.part - b.part.part ); }); const fileParts = [ ftyp, mergedMoov ]; let outputOffset = ftyp.byteLength + mergedMoov.byteLength; let sequenceNumber = 1; for(const fragment of fragments) { const source = toUint8( fragment.part.data ); const moof = findFirstBox( source, 'moof' ); if(!moof) { throw new Error( 'Fragment has no moof' ); } const outputMoofOffset = outputOffset + moof.offset; const entry = fragment.track; const rewritten = rewriteFragment( source, entry.id, entry.parsed[0].info.decodeStart, entry.targetFirstTfdt, sequenceNumber++, outputMoofOffset ); fileParts.push( rewritten ); outputOffset += rewritten.byteLength; } const file = new Blob( fileParts, { type: recordVideo ? 'video/mp4' : 'audio/mp4' } ); const objectURL = URL.createObjectURL(file); const lastPart = baseTrack.parts[baseTrack.parts.length - 1]; const duration = lastPart.start + lastPart.duration - baseTrack.parts[0].start; const date = snapshotDate || new Date(snapshotTime - duration * 1000); const filename = getFileName( fname, '.mp4', date ); await new Promise((resolve, reject) => { let settled = false; function finish(error = null) { if(settled) { return; } settled = true; setTimeout(() => { URL.revokeObjectURL( objectURL ); }, 1000); if(error) { reject(error); return; } if(state) { if(lastVideoPart && exportedTracks.some(entry => entry.type === 'video')) { state.video = {sn: lastVideoPart.sn, part: lastVideoPart.part, captureOrder: lastVideoPart.captureOrder}; } if(lastAudioPart && exportedTracks.some(entry => entry.type === 'audio')) { state.audio = {sn: lastAudioPart.sn, part: lastAudioPart.part, captureOrder: lastAudioPart.captureOrder}; } } resolve(); } try { download({ url: objectURL, name: filename, onload: () => finish(), onloadend: () => finish(), onerror: () => finish(new Error('Rewind download failed')), ontimeout: () => finish(new Error('Rewind download timed out')) }); } catch(error) { finish(error); } }); } function pause() { if(previewCanvas) { const elPreviewVideo = isLive ? liveVideo : rewindVideo; if(elPreviewVideo) { captureVideoFrame(elPreviewVideo, previewCanvas); copyTransformStyle(previewCanvas, elPreviewVideo); freezoomCopy(gFreezoomInstances.get(previewCanvas), gFreezoomInstances.get(elPreviewVideo)); } } rewindVideo.pause(); rewindVideo.style.visibility = 'hidden'; if(isLive) { pausedLiveMuted = liveVideo.muted; } bIsPaused = true; liveVideo.muted = true; liveVideo.style.visibility = 'hidden'; if(liveVideo.paused) { playVideoIgnoreErrors(liveVideo); } updatePauseTime(); } function updatePauseTime() { if(bIsPaused) { pauseTime = Date.now() / 1000; return; } pauseTime = null; } async function play() { if( bIsPaused && pauseTime !== null ) { copyTransformStyle( liveVideo, previewCanvas ); freezoomCopy( gFreezoomInstances.get( liveVideo ), gFreezoomInstances.get( previewCanvas ) ); copyTransformStyle( rewindVideo, previewCanvas ); freezoomCopy( gFreezoomInstances.get( rewindVideo ), gFreezoomInstances.get( previewCanvas ) ); let rewindTo = Date.now() / 1000 - pauseTime; if(rewindedTo !== null) { rewindTo += rewindedTo; } if(rewindTo < 5 && 0) { live(); } else { await rewind( rewindTo ); } } if(!isLive && rewindVideo) { playVideoIgnoreErrors(rewindVideo); rewindVideo.style.visibility = 'visible'; } if(isLive && liveVideo) { bIsPaused = false; liveVideo.muted = pausedLiveMuted ?? liveVideo.muted; pausedLiveMuted = null; playVideoIgnoreErrors(liveVideo); liveVideo.style.visibility = 'visible'; } bIsPaused = false; } async function playPause() { if(bIsPaused) { await play(); return; } pause(); } function getPauseTime() { if(!bIsPaused) { return null; } return pauseTime; } function isPaused() { return bIsPaused; } function getRewindTo() { return rewindedTo; } async function rewind(seconds) { if(isRewinding) { return; } isRewinding = true; updatePauseTime(); try { if(previewCanvas) { const elPreviewVideo = isLive ? liveVideo : rewindVideo; if(elPreviewVideo) { captureVideoFrame(elPreviewVideo, previewCanvas); if(!bIsPaused) { copyTransformStyle(previewCanvas, elPreviewVideo); } } } if(!videoInit || !audioInit) { const generation = initGeneration; const init = await loadInitSegments(); if(generation !== initGeneration) { return; } videoInit ??= init.video; audioInit ??= init.audio; } if(!videoInit || !audioInit) { throw new Error( 'HLS init segments unavailable' ); } if( !videoParts.length || !audioParts.length ) { throw new Error( 'no media data' ); } seconds = Math.max( 0, Math.min( seconds, getDuration() ) ); await createPlayback( seconds ); } finally { isRewinding = false; } isLive = false; rewindedTo = seconds; pausedLiveMuted = null; } function stopPump() { const session = pump; if(!session) { return; } if(session.rewindVideo) { session.rewindVideo.removeEventListener( 'timeupdate', session.onTimeUpdate ); } session.cancelled = true; pump = null; } function bufferedAhead( sourceBuffer, time ) { const { buffered } = sourceBuffer; for( let i = 0; i < buffered.length; i++ ) { if( time >= buffered.start(i) && time <= buffered.end(i) ) { return ( buffered.end(i) - time ); } } return 0; } function totalBufferedDuration( sourceBuffer ) { const { buffered } = sourceBuffer; let total = 0; for( let i = 0; i < buffered.length; i++ ) { total += buffered.end(i) - buffered.start(i); } return total; } function isSourceBufferUsable( sourceBuffer, mediaSource ) { return !!( sourceBuffer && mediaSource && mediaSource.readyState === 'open' && !sourceBuffer.updating ); } function isQuotaError(error) { if( error?.name === 'QuotaExceededError' ) { return true; } const message = String( error?.message || error || '' ).toLowerCase(); return ( message.includes( 'sourcebuffer' ) && ( message.includes( 'quota' ) || message.includes( 'buffer is full' ) || message.includes( 'buffer not sufficient' ) || message.includes( 'not enough buffer' ) ) ); } function isDetachedError(error) { if( !error || typeof error !== 'object' ) { return false; } const name = String( error.name || '' ); if( name === 'InvalidStateError' || name === 'AbortError' ) { return true; } const message = String( error.message || '' ).toLowerCase(); return ( message.includes( 'media source' ) && ( message.includes( 'closed' ) || message.includes( 'detached' ) || message.includes( 'not open' ) ) ); } function removeRange( sourceBuffer, start, end, mediaSource ) { if( end <= start || !isSourceBufferUsable( sourceBuffer, mediaSource ) ) { return Promise.resolve(); } return new Promise( (resolve, reject) => { let settled = false; const cleanup = () => { sourceBuffer.removeEventListener( 'updateend', done ); sourceBuffer.removeEventListener( 'error', fail ); sourceBuffer.removeEventListener( 'abort', fail ); }; const done = () => { if(settled) { return; } settled = true; cleanup(); resolve(); }; const fail = event => { if(settled) { return; } settled = true; cleanup(); reject( event instanceof Event ? new DOMException( 'SourceBuffer remove failed', 'AbortError' ) : event ); }; sourceBuffer.addEventListener( 'updateend', done ); sourceBuffer.addEventListener( 'error', fail ); sourceBuffer.addEventListener( 'abort', fail ); try { if( !isSourceBufferUsable( sourceBuffer, mediaSource ) ) { cleanup(); resolve(); return; } sourceBuffer.remove( start, end ); } catch(error) { cleanup(); reject(error); } } ); } async function trimBehind( sourceBuffer, time, aggressive, mediaSource ) { if( !isSourceBufferUsable( sourceBuffer, mediaSource ) || !sourceBuffer.buffered.length ) { return false; } const margin = aggressive ? 1 : BUFFER_BEHIND_SECONDS; const removeEnd = time - margin; let removed = false; for( let i = 0; i < sourceBuffer.buffered.length; i++ ) { const start = sourceBuffer.buffered.start(i); const end = sourceBuffer.buffered.end(i); const actualEnd = Math.min( removeEnd, end, time ); if(actualEnd <= start) { continue; } await removeRange( sourceBuffer, start, actualEnd, mediaSource ); removed = true; break; } return removed; } async function appendNext( session, sourceBuffer, parts, indexKey, time ) { if( session.cancelled || pump !== session || !isSourceBufferUsable( sourceBuffer, session.mediaSource ) ) { return false; } const index = session[indexKey]; const part = parts[index]; if(!part) { return false; } let quotaRetries = 0; while( !session.cancelled && pump === session ) { if( !isSourceBufferUsable( sourceBuffer, session.mediaSource ) ) { return false; } try { await append( sourceBuffer, part.data, session.mediaSource ); if( session.cancelled || pump !== session ) { return false; } if( !isSourceBufferUsable( sourceBuffer, session.mediaSource ) ) { return false; } session[indexKey]++; return true; } catch(error) { if( session.cancelled || pump !== session ) { return false; } if( !isSourceBufferUsable( sourceBuffer, session.mediaSource ) || isDetachedError(error) ) { return false; } if(!isQuotaError(error)) { throw error; } quotaRetries++; if(time <= 0.001) { session.aheadTarget = Math.max( MIN_BUFFER_AHEAD_SECONDS, session.aheadTarget * 0.5 ); if( quotaRetries >= MAX_QUOTA_RETRIES ) { throw error; } if( part.data.byteLength > session.maxSinglePartBytes ) { throw error; } await yieldToMainThread(); continue; } const removed = await trimBehind( sourceBuffer, time, true, session.mediaSource ); if( session.cancelled || pump !== session ) { return false; } if(!removed) { session.aheadTarget = Math.max( MIN_BUFFER_AHEAD_SECONDS, session.aheadTarget * 0.5 ); if( quotaRetries >= MAX_QUOTA_RETRIES ) { throw error; } await yieldToMainThread(); continue; } if( quotaRetries >= MAX_QUOTA_RETRIES ) { throw error; } await yieldToMainThread(); } } return false; } async function initialFill(session) { while( session.videoIndex < session.videos.length && isSourceBufferUsable( session.videoBuffer, session.mediaSource ) && totalBufferedDuration( session.videoBuffer ) < session.aheadTarget ) { const appended = await appendNext( session, session.videoBuffer, session.videos, 'videoIndex', 0 ); if(!appended) { return; } if( pump !== session ) { return; } } while( session.audioIndex < session.audios.length && isSourceBufferUsable( session.audioBuffer, session.mediaSource ) && totalBufferedDuration( session.audioBuffer ) < session.aheadTarget ) { const appended = await appendNext( session, session.audioBuffer, session.audios, 'audioIndex', 0 ); if(!appended) { return; } if( pump !== session ) { return; } } } async function pumpTrack( session, sourceBuffer, parts, indexKey, time ) { while( session[indexKey] < parts.length && isSourceBufferUsable( sourceBuffer, session.mediaSource ) && bufferedAhead( sourceBuffer, time ) < session.aheadTarget ) { const oldIndex = session[indexKey]; const appended = await appendNext( session, sourceBuffer, parts, indexKey, time ); if(!appended) { return false; } if( session[indexKey] === oldIndex ) { return false; } if( pump !== session || session.cancelled ) { return false; } if( !isSourceBufferUsable( sourceBuffer, session.mediaSource ) ) { return false; } if( bufferedAhead( sourceBuffer, time ) >= session.aheadTarget ) { break; } } return true; } async function pumpTick() { const session = pump; if( !session || session.busy || session.cancelled ) { return; } if( !isSourceBufferUsable( session.videoBuffer, session.mediaSource ) || !isSourceBufferUsable( session.audioBuffer, session.mediaSource ) ) { return; } session.busy = true; try { if( session.mediaSource.readyState !== 'open' ) { return; } const time = session.rewindVideo.currentTime; await trimBehind( session.videoBuffer, time, false, session.mediaSource ); if( pump !== session || session.cancelled || !isSourceBufferUsable( session.videoBuffer, session.mediaSource ) || !isSourceBufferUsable( session.audioBuffer, session.mediaSource ) ) { return; } await trimBehind( session.audioBuffer, time, false, session.mediaSource ); if( pump !== session || session.cancelled || !isSourceBufferUsable( session.videoBuffer, session.mediaSource ) || !isSourceBufferUsable( session.audioBuffer, session.mediaSource ) ) { return; } await pumpTrack( session, session.videoBuffer, session.videos, 'videoIndex', time ); if( pump !== session || session.cancelled || !isSourceBufferUsable( session.videoBuffer, session.mediaSource ) || !isSourceBufferUsable( session.audioBuffer, session.mediaSource ) ) { return; } await pumpTrack( session, session.audioBuffer, session.audios, 'audioIndex', time ); if( pump !== session || session.cancelled ) { return; } if( session.videoIndex >= session.videos.length && session.audioIndex >= session.audios.length ) { if( session.mediaSource.readyState === 'open' ) { try { session.mediaSource.endOfStream(); } catch(error) { if( pump === session ) { console.error( 'rewind endOfStream failed', error ); } } } if(pump === session) { stopPump(); } } } catch(error) { if( pump !== session || session.cancelled ) { return; } if( !isSourceBufferUsable( session.videoBuffer, session.mediaSource ) || !isSourceBufferUsable( session.audioBuffer, session.mediaSource ) || isDetachedError(error) ) { stopPump(); return; } console.error( 'rewind pump failed', error ); stopPump(); } finally { session.busy = false; } } async function createPlayback(seconds) { const mySession = ++activeSession; stopPump(); let sourceUrl = null; try { const level = hls.levels[videoLevel]; if(!level) { return; } const audio = hls.audioTracks[ hls.audioTrack ]; const videoCodec = level.videoCodec; const audioCodec = audio.audioCodec; const existingBuffers = hls.media?.sourceBuffers || hls.coreComponents?.find( x => x.sourceBuffers )?.sourceBuffers; const existingVideoBuffer = existingBuffers?.find( x => x[0] === 'video' )?.[1]; const existingAudioBuffer = existingBuffers?.find( x => x[0] === 'audio' )?.[1]; if( !existingVideoBuffer || !existingAudioBuffer ) { throw new Error( 'Could not find HLS SourceBuffers' ); } const videoTimestampOffset = existingVideoBuffer.timestampOffset; const audioTimestampOffset = existingAudioBuffer.timestampOffset; rewindVideo.pause(); const oldUrl = rewindVideo.src; rewindVideo.removeAttribute( 'src' ); rewindVideo.load(); if( oldUrl?.startsWith('blob:') ) { URL.revokeObjectURL( oldUrl ); } const source = new MediaSource(); sourceUrl = URL.createObjectURL( source ); rewindVideo.src = sourceUrl; await once( source, 'sourceopen' ); if( mySession !== activeSession ) { return; } const videoBuffer = source.addSourceBuffer( `video/mp4; codecs="${videoCodec}"` ); const audioBuffer = source.addSourceBuffer( `audio/mp4; codecs="${audioCodec}"` ); videoBuffer.timestampOffset = videoTimestampOffset; audioBuffer.timestampOffset = audioTimestampOffset; const firstVideo = videoParts[0]; const lastVideo = videoParts[ videoParts.length - 1 ]; if( !firstVideo || !lastVideo ) { throw new Error( 'No video parts available' ); } const cacheStart = firstVideo.start; const cacheEnd = lastVideo.start + lastVideo.duration; const cacheDuration = cacheEnd - cacheStart; seconds = Math.max( 0, Math.min( seconds, cacheDuration ) ); const targetOriginal = cacheEnd - seconds; let videoStartIndex = videoParts.findIndex( part => part.start + part.duration > targetOriginal ); if(videoStartIndex < 0) { videoStartIndex = videoParts.length - 1; } if(videoStartIndex > 0) { videoStartIndex--; } const videos = videoParts.slice( videoStartIndex ); if(!videos.length) { throw new Error( 'Not enough video data for rewind' ); } const videoStart = videos[0].start; let audioStartIndex = audioParts.findIndex( part => part.start + part.duration > targetOriginal ); if(audioStartIndex < 0) { audioStartIndex = audioParts.length - 1; } if(audioStartIndex > 0) { audioStartIndex--; } const audios = audioParts.slice( audioStartIndex ); if(!audios.length) { throw new Error( 'Not enough audio data for rewind' ); } await append( videoBuffer, videoInit, source ); if( mySession !== activeSession ) { return; } await append( audioBuffer, audioInit, source ); if( mySession !== activeSession ) { return; } const session = { rewindVideo, mediaSource: source, sourceUrl, videoBuffer, audioBuffer, videos, audios, videoIndex: 0, audioIndex: 0, aheadTarget: INITIAL_BUFFER_AHEAD_SECONDS, maxSinglePartBytes: Math.max( 1, Math.min( 32 * 1024 * 1024, ( videos.reduce( (max, part) => Math.max( max, part.data.byteLength ), 0 ) ) ) ), busy: false, cancelled: false, onTimeUpdate: () => pumpTick() }; session.normalAheadTarget = BUFFER_AHEAD_SECONDS; pump = session; await initialFill( session ); if( mySession !== activeSession || pump !== session ) { return; } session.aheadTarget = Math.min( session.normalAheadTarget, Math.max( session.aheadTarget, MIN_BUFFER_AHEAD_SECONDS ) ); liveVideo.style.visibility = 'hidden'; const liveVideoWasMuted = pausedLiveMuted ?? liveVideo.muted; liveVideo.muted = true; let elCopyTransformPropertiesFrom; if(isLive) { elCopyTransformPropertiesFrom = liveVideo; } else if(bIsPaused) { elCopyTransformPropertiesFrom = previewCanvas; } if(!bIsPaused) { rewindVideo.style.visibility = 'visible'; } if(elCopyTransformPropertiesFrom) { copyTransformStyle(rewindVideo, elCopyTransformPropertiesFrom); freezoomCopy( gFreezoomInstances.get( rewindVideo ), gFreezoomInstances.get( elCopyTransformPropertiesFrom ) ); } await waitForMetadata(); if( mySession !== activeSession || pump !== session ) { return; } const buffered = []; for( let i = 0; i < rewindVideo.buffered.length; i++ ) { buffered.push([ rewindVideo.buffered.start(i), rewindVideo.buffered.end(i) ]); } if(!buffered.length) { throw new Error( 'Rewind MediaSource has no buffered video' ); } const mseStart = buffered[0][0]; const targetOffset = targetOriginal - videoStart; let seekTarget = mseStart + targetOffset; let targetRange = null; for( const range of buffered ) { if( seekTarget >= range[0] && seekTarget <= range[1] ) { targetRange = range; break; } } if(!targetRange) { let closest = buffered[0][0]; let distance = Math.abs( seekTarget - closest ); for( const range of buffered ) { const candidates = [ range[0], range[1] ]; for( const candidate of candidates ) { const candidateDistance = Math.abs( seekTarget - candidate ); if( candidateDistance < distance ) { closest = candidate; distance = candidateDistance; } } } seekTarget = closest; } rewindVideo.currentTime = seekTarget; if(pump === session) { rewindVideo.addEventListener('timeupdate', session.onTimeUpdate); } if(!bIsPaused) { playVideoIgnoreErrors(rewindVideo); } if(isLive) { rewindVideo.muted = liveVideoWasMuted; rewindVideo.volume = liveVideo.volume; } queueMicrotask(() => { if( pump === session && !session.cancelled ) { pumpTick(); } }); } catch(error) { if( mySession !== activeSession ) { return; } throw error; } } function append( sourceBuffer, data, mediaSource ) { if( !isSourceBufferUsable( sourceBuffer, mediaSource ) ) { return Promise.reject( new DOMException( 'SourceBuffer is not attached to an open MediaSource', 'InvalidStateError' ) ); } return new Promise( (resolve, reject) => { let settled = false; const cleanup = () => { sourceBuffer.removeEventListener( 'updateend', done ); sourceBuffer.removeEventListener( 'error', fail ); sourceBuffer.removeEventListener( 'abort', fail ); }; const done = () => { if(settled) { return; } settled = true; cleanup(); resolve(); }; const fail = event => { if(settled) { return; } settled = true; cleanup(); reject( event instanceof Event ? new DOMException( 'SourceBuffer append failed', 'QuotaExceededError' ) : event ); }; sourceBuffer.addEventListener( 'updateend', done ); sourceBuffer.addEventListener( 'error', fail ); sourceBuffer.addEventListener( 'abort', fail ); try { if( !isSourceBufferUsable( sourceBuffer, mediaSource ) ) { cleanup(); reject( new DOMException( 'SourceBuffer is no longer usable', 'InvalidStateError' ) ); return; } sourceBuffer.appendBuffer( data ); } catch(error) { cleanup(); reject(error); } } ); } function yieldToMainThread() { return new Promise( resolve => setTimeout( resolve, 0 ) ); } function waitForMetadata() { if(rewindVideo.readyState >= 1) { return Promise.resolve(); } return once( rewindVideo, 'loadedmetadata' ); } function once(target, event) { return new Promise( resolve => { target.addEventListener( event, resolve, { once: true } ); } ); } function copyData(data) { if(data instanceof ArrayBuffer) { return data.slice(0); } if(ArrayBuffer.isView(data)) { return data.buffer.slice( data.byteOffset, data.byteOffset + data.byteLength ); } throw new TypeError( 'Expected ArrayBuffer or TypedArray' ); } async function live() { if(!rewindVideo || !liveVideo) { return; } ++activeSession; stopPump(); copyTransformStyle( liveVideo, rewindVideo ); freezoomCopy( gFreezoomInstances.get( liveVideo ), gFreezoomInstances.get( rewindVideo ) ); const playbackVideo = isLive ? liveVideo : rewindVideo; const muted = isLive ? (pausedLiveMuted ?? playbackVideo.muted) : playbackVideo.muted; const volume = playbackVideo.volume; isLive = true; rewindedTo = null; bIsPaused = false; pauseTime = null; pausedLiveMuted = null; rewindVideo.muted = true; rewindVideo.pause(); const oldUrl = rewindVideo.src; rewindVideo.removeAttribute('src'); rewindVideo.load(); if(oldUrl?.startsWith('blob:')) { URL.revokeObjectURL( oldUrl ); } rewindVideo.style.visibility = 'hidden'; liveVideo.muted = muted; liveVideo.volume = volume; const liveSyncPosition = hls?.liveSyncPosition; if(Number.isFinite(liveSyncPosition)) { liveVideo.currentTime = liveSyncPosition; } if(liveVideo.paused) { playVideoIgnoreErrors(liveVideo); } liveVideo.style.visibility = 'visible'; if(typeof onLive === 'function') { onLive(); } } function destroy() { stopRecordingSession(); ++initGeneration; ++activeSession; stopPump(); if(hls) { hls.off( Hls.Events.BUFFER_APPENDING, onBufferAppending ); hls.off( Hls.Events.LEVEL_SWITCHED, onLevelSwitched ); } let muted = null; let volume = null; if(rewindVideo) { setFreezoomEnabled( elVideoHolder, false, rewindVideo ); if(!isLive) { muted = rewindVideo.muted; volume = rewindVideo.volume; } rewindVideo.muted = true; rewindVideo.pause(); const oldUrl = rewindVideo.src; rewindVideo.removeAttribute( 'src' ); rewindVideo.load(); if( oldUrl?.startsWith('blob:') ) { URL.revokeObjectURL( oldUrl ); } rewindVideo.removeEventListener( 'ended', onRewindEnded ); rewindVideo.remove(); } if(liveVideo) { liveVideo.removeEventListener( 'volumechange', liveVideoOnVolumeChange ); liveVideo.removeEventListener( 'play', liveVideoOnPlay ); liveVideo.removeEventListener( 'timeupdate', liveVideoOnTimeUpdate ); liveVideo.style.visibility = 'visible'; if(isLive && pausedLiveMuted !== null) { muted = pausedLiveMuted; volume = liveVideo.volume; } if(muted !== null && volume !== null) { liveVideo.muted = muted; liveVideo.volume = volume; } } if(previewCanvas) { setFreezoomEnabled( elVideoHolder, false, previewCanvas ); previewCanvas.remove(); } hls = null; liveVideo = null; rewindVideo = null; previewCanvas = null; videoInit = null; audioInit = null; initPromise = null; videoContinuity = null; videoParts = []; audioParts = []; incompleteAppends = {video: null, audio: null}; mediaClocks = {video: null, audio: null}; mediaAnchors = {video: null, audio: null}; nextCaptureOrder = 0; videoLevel = -1; totalBytes = 0; downloadState = { video: null, audio: null, busy: false }; screenshots = []; screenshotBytes = 0; lastScreenshotMediaTime = null; screenshotCaptureBusy = false; screenshotCanvas = null; screenshotContext = null; pausedLiveMuted = null; } function getBufferedRanges( sourceBuffer ) { const ranges = []; for( let i = 0; i < sourceBuffer.buffered.length; i++ ) { ranges.push({ start: sourceBuffer.buffered.start(i), end: sourceBuffer.buffered.end(i) }); } return ranges; } return { init, rewind, live, destroy, getDuration, playPause, getPauseTime, isPaused, getRewindTo, getData, startRecordingSession, stopRecordingSession, dump, getDownloadInfo, getScreenshot, setMaxDuration, getMaxDuration, getSize, isRewinded }; } const defaultInstance = create(); return { create, init: defaultInstance.init, rewind: defaultInstance.rewind, live: defaultInstance.live, destroy: defaultInstance.destroy, getDuration: defaultInstance.getDuration, playPause: defaultInstance.playPause, isPaused: defaultInstance.isPaused, getPauseTime: defaultInstance.getPauseTime, getRewindTo: defaultInstance.getRewindTo, getData: defaultInstance.getData, dump: defaultInstance.dump, getDownloadInfo: defaultInstance.getDownloadInfo, getScreenshot: defaultInstance.getScreenshot, setMaxDuration: defaultInstance.setMaxDuration, getMaxDuration: defaultInstance.getMaxDuration, getSize: defaultInstance.getSize, isRewinded: defaultInstance.isRewinded }; })(); function blurMuteVideo(elVideo) { const currentMutedSettingValue = getSetting('vm'); elVideo.muted = true; gWasMutedBySetting = true; runNextTick(() => { if(currentMutedSettingValue) { return; } setSetting('vm', false); }); } function muteAllOtherTabs() { if(!gBroadcastChannel) { return; } gBroadcastChannel.postMessage({ 'type': 'mute' }); } function resetVideoAudioBoost(elVideoHolder) { elVideoHolder._cnh_videoAudioBost = { boost: 1, voiceBoost: 1 }; updateVideoAudioBoost(elVideoHolder); } function updateVideoAudioBoost(elVideoHolder) { setVideoAudioBoost(elVideoHolder, elVideoHolder._cnh_videoAudioBost); } function setVideoAudioBoost(elVideoHolder, options) { const video = getVideoHolderVideo(elVideoHolder); if(!video) { return; } if(!hasUserInteracted()) { return; } if(!options) { options = { boost: 1, voiceBoost: 1 }; elVideoHolder._cnh_videoAudioBost = options; } if(!video.__audioGraph) { let audioCtx; try { audioCtx = new AudioContext(); } catch(e) { return; } if(!audioCtx) { return; } const source = audioCtx.createMediaElementSource(video); const compressor = audioCtx.createDynamicsCompressor(); compressor.threshold.value = -20; compressor.knee.value = 25; compressor.ratio.value = 4; compressor.attack.value = 0.003; compressor.release.value = 0.25; const highpass = audioCtx.createBiquadFilter(); highpass.type = 'highpass'; highpass.frequency.value = 90; const lowMidCut = audioCtx.createBiquadFilter(); lowMidCut.type = 'peaking'; lowMidCut.frequency.value = 450; lowMidCut.Q.value = 1; lowMidCut.gain.value = 0; const presence = audioCtx.createBiquadFilter(); presence.type = 'peaking'; presence.frequency.value = 3200; presence.Q.value = 1; presence.gain.value = 0; const gain = audioCtx.createGain(); gain.gain.value = 1; source .connect(compressor) .connect(highpass) .connect(lowMidCut) .connect(presence) .connect(gain) .connect(audioCtx.destination); video.__audioGraph = { audioCtx, source, highpass, lowMidCut, presence, compressor, gain }; } const g = video.__audioGraph; if(options.boost != null) { g.gain.gain.value = options.boost; } if(options.voiceBoost != null) { const v = options.voiceBoost; g.lowMidCut.gain.value = -v * 0.4; g.presence.gain.value = v * 0.8; } if(options.compressor != null) { g.compressor.threshold.value = options.compressor.threshold ?? g.compressor.threshold.value; g.compressor.ratio.value = options.compressor.ratio ?? g.compressor.ratio.value; } } function isThereCurrentVideo() { return getCurrentVideo() !== null; } function isVideoPlaying(vid) { return !vid.paused && !vid.ended && vid.readyState > 2; } function playVideoIgnoreErrors(elVideo) { if(elVideo) { elVideo.play().catch(() => {}); } } function exitVideoPiP() { if(!document.pictureInPictureElement) { return; } document.exitPictureInPicture(); document.querySelectorAll('video').forEach(elVideo => { playVideoIgnoreErrors(elVideo); }); } function runNextTick(callback) { setTimeout(callback, 1); } function makeSureRuns(callback, intvs = []) { callback(); requestAnimationFrame(callback); if(intvs.length === 0) { intvs = [1, 10, 25, 50, 100, 200, 250, 500]; } for(const intv of intvs) { setTimeout(callback, intv); } } function hideElement(el) { if(!el) { return; } el.style.display = 'none'; } function showElement(el) { if(!el) { return; } let displayValue = 'block'; if(el.nodeName === 'TR') { displayValue = 'table-row'; } el.style.display = displayValue; } function isElementHidden(el) { return getComputedStyle(el).display === 'none'; } function hasOverlayScrollbar(scroller = document.scrollingElement) { if(!scroller || scroller.scrollHeight <= scroller.clientHeight) { return false; } const scrollbarWidth = scroller === document.scrollingElement ? window.innerWidth - document.documentElement.clientWidth : scroller.offsetWidth - scroller.clientWidth; return scrollbarWidth === 0; } function toggleElement(el, show = null) { if(show === null) { show = isElementHidden(el); } if(show) { showElement(el); } else { hideElement(el); } } function toggleElementVisibility(el, show = null) { if(show === null) { show = getComputedStyle(el).visibility === 'hidden'; } el.style.visibility = show ? 'visible' : 'hidden'; } function getElementIndex(el) { return Array.from(el.parentElement.children).indexOf(el); } function getElementOffset(el) { const rect = el.getBoundingClientRect(); return { top: rect.top + window.scrollY, left: rect.left + window.scrollX }; } function queryHideElement(selector) { queryAndRun(selector, hideElement); } function queryShowElement(selector) { queryAndRun(selector, showElement); } function queryToggleElement(selector, show = null) { queryAndRun(selector, (el) => { toggleElement(el, show); }); } function queryAndRun(selector, callback) { document.querySelectorAll(selector).forEach(el => callback(el)); } function isElementVisuallyEmpty(node) { if(node.hidden) { return true; } if(node.nodeType === Node.TEXT_NODE) { return !node.textContent.trim(); } if(node.nodeType !== Node.ELEMENT_NODE) { return true; } const style = getComputedStyle(node); if(style.display === 'none' || style.visibility === 'hidden') { return true; } if(node.childNodes.length === 0) { if(node.nodeName === 'BR') { return true; } if(node.nodeName !== 'IMG' && !node.textContent.trim()) { return true; } return false; } for(const node2 of node.childNodes) { if(!isElementVisuallyEmpty(node2)) { return false; } } return true; } function clearElement(el) { el.innerHTML = ''; } function createLocalizedElement(type, name) { const el = document.createElement(type); el.className = 'ce-loc'; const loc = name.replaceAll(' ', '_').toLowerCase(); el.setAttribute('data-ce-loc', loc); el.innerText = getLocale(name); return el; } function createLocalizedSpan(name) { return createLocalizedElement('span', name); } function updateVolumeSettings(elVideo) { if(hasUserInteracted()) { setSetting('vm', elVideo.muted); } } function formatTime(inputSeconds) { const hours = Math.floor(inputSeconds / 3600); const minutes = Math.floor((inputSeconds % 3600) / 60); const seconds = inputSeconds % 60; const mm = String(minutes).padStart(2, '0'); const ss = String(seconds).padStart(2, '0'); return hours + ':' + mm + ':' + ss; } function formatTimePretty(inputSeconds) { if(inputSeconds < 60) { return getLocale('less_than_minute'); } const minute = 60; const hour = 60 * minute; const day = 24 * hour; const week = 7 * day; const month = 30 * day; const year = 365 * day; const years = Math.floor(inputSeconds / year); inputSeconds %= year; const months = Math.floor(inputSeconds / month); inputSeconds %= month; const weeks = Math.floor(inputSeconds / week); inputSeconds %= week; const days = Math.floor(inputSeconds / day); inputSeconds %= day; const hours = Math.floor(inputSeconds / hour); inputSeconds %= hour; const minutes = Math.floor(inputSeconds / minute); const parts = []; function push(value, singular, plural) { if(!value || parts.length >= 2) { return; } parts.push(value + ' ' + (value === 1 ? singular : plural)); } push(years, 'year', 'years'); push(months, 'month', 'months'); push(weeks, 'week', 'weeks'); push(days, 'day', 'days'); push(hours, 'hour', 'hours'); push(minutes, 'minute', 'minutes'); return parts.join(', '); } function formatNumber(num) { if(typeof Intl === 'undefined') { return null; } const lang = getSiteLang(); return new Intl.NumberFormat(lang, { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(num); } function getVideoHolderVideo(elVideoHolder) { if(!elVideoHolder) { return; } let elVideo = elVideoHolder.querySelector('video'); if(!elVideo) { elVideo = elVideoHolder.querySelector('.cb-enh-multi-video-iframe')?.contentDocument?.querySelector('video[data-testid="video"]'); } return elVideo; } function getVideoHolderVideoOrRewindVideo(elVideoHolder) { if(!elVideoHolder) { return; } const broadcasterName = getCurrentBroadcasterName(); if(broadcasterName && gRewindObjs[broadcasterName]) { if(gRewindObjs[broadcasterName].rewind.isPaused()) { return elVideoHolder.querySelector('.cb-enh-rewind-video-canvas'); } if(gRewindObjs[broadcasterName].rewind.isRewinded()) { return getVideoHolderRewindVideo(elVideoHolder); } } return getVideoHolderVideo(elVideoHolder); } function getVideoHolderRewindVideo(elVideoHolder) { if(!elVideoHolder) { return; } return elVideoHolder.querySelector('video.cb-enh-rewind-video'); } function updateVideoFilters(elVideoHolder) { const elVideo = getVideoHolderVideoOrRewindVideo(elVideoHolder); if(!elVideo) { return; } let filters = ''; for(const v of elVideoHolder._cnh_videoFilters) { if(v) { filters += v[1] + ' '; } } filters = filters.trim(); elVideo.style.filter = filters; } function resetVideoFilters(elVideoHolder) { if(!elVideoHolder) { return; } elVideoHolder._cnh_videoFilters = []; updateVideoFilters(elVideoHolder); const elVideo = getVideoHolderVideoOrRewindVideo(elVideoHolder); if(elVideo) { updateCurrentVideoTransformStyle(elVideoHolder, { 'mirrored': false, 'inverted': false, 'rotation': null }); } } const acre1 = '==gIng2NSVmZnAi'; function enhanceInaccessibleRoom() { const lang = getSiteLang(); const elBaseRoomContentDiv = document.querySelector('div.BaseRoomContents div'); if(!elBaseRoomContentDiv) { return; } if(elBaseRoomContentDiv.textContent.indexOf('Access denied') !== 0) { return; } elBaseRoomContentDiv.insertAdjacentHTML('beforeend', '<br><span class="ce-loc" data-ce-loc="try_load">Chaturbate Enhancer will load video and bio of this room.</span><br><br>'); const elForm = document.querySelector("form[action='/set_language/'] input[name='next']"); if(!elForm) { return; } const username = elForm.value.slice(1, -1); gCurrentBroadcaster = username; gCurrentRoomIsInaccessible = true; if(lang === 'en') { getJSON('https://' + gBaseDomain + '/api/chatvideocontext/' + username + '/', function(data) { if(data.status === 403 && data.detail && data.detail === 'You are banned from this room.') { localStorage.setItem('cb-enh-n', username); window.location.href = '/?i='; return; } enhanceInaccessibleRoom2(); }); } else { enhanceInaccessibleRoom2(); } } function enhanceInaccessibleRoom2() { const elForm = document.querySelector("form[action='/set_language/'] input[name='next']"); if(!elForm) { return; } const lang = getSiteLang(); const username = elForm.value.slice(1, -1); gCurrentBroadcaster = username; gCurrentRoomIsInaccessible = true; const elBaseRoomContentDiv = document.querySelector('div.BaseRoomContents div'); if(!elBaseRoomContentDiv) { return; } if(elBaseRoomContentDiv.textContent.indexOf('Access denied') !== 0) { return; } elBaseRoomContentDiv.insertAdjacentHTML('beforeend', '<br><span class="ce-loc" data-ce-loc="try_load">Chaturbate Enhancer will load video and bio of this room.</span><br><br>'); addStyle(`.BaseRoomContents div:not(#cb-enh-manager-feature-error) {font-size: 14px !important;}.BaseRoomContents div {color: #f7f7f7;font-family: UbuntuMedium, Arial, Helvetica, sans-serif;font-weight: normal;}.darkmode .BaseRoomContents {border-color: transparent !important;background-color: #202c39 !important;}.ce-row-1 {color: #0a5a83;}.darkmode .ce-row-1 {color: white;}`); const upperHolder = document.createElement('div'); elBaseRoomContentDiv.appendChild(upperHolder); const videoHolder = document.createElement('div'); elBaseRoomContentDiv.appendChild(videoHolder); const upperHolder2 = document.createElement('div'); elBaseRoomContentDiv.appendChild(upperHolder2); const upperHolder3 = document.createElement('div'); elBaseRoomContentDiv.appendChild(upperHolder3); const infoHolder = document.createElement('div'); elBaseRoomContentDiv.appendChild(infoHolder); const infoHolder2 = document.createElement('div'); elBaseRoomContentDiv.appendChild(infoHolder2); let isOnline = false; const elSpInfo = document.createElement('div'); elSpInfo.id = gtrrft('mspr'); elSpInfo.style.marginTop = '0'; elSpInfo.style.marginBottom = '10px'; infoHolder.append(elSpInfo); multiInitSupportInfo(); xmlhttpRequest({ method: 'GET', url: 'https://cb-enh-api2.improper.dev/api/room/' + username + '?key=' + acre7, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Referer': 'https://' + window.location.hostname + '/' + username + '/', }, timeout: 60*1*1000, onload: function(resp) { let data; try { data = JSON.parse(resp.responseText); } catch(e) { return; } if(!elBaseRoomContentDiv) { return; } let playVideo = true; if(data['room_status'] !== 'public') { const avDiv = document.createElement("div"); upperHolder.appendChild(avDiv); insertRoomAv(avDiv, username); const statusSpan = document.createElement('span'); statusSpan.className = 'ce-loc'; statusSpan.setAttribute('data-ce-loc', `status_${data['room_status']}`); statusSpan.textContent = data['room_status']; upperHolder.append( Object.assign(document.createElement('span'), { className: 'ce-loc ce-row-1', textContent: 'Room status is' }), document.createTextNode(': '), statusSpan, document.createElement('br') ); playVideo = false; } else { isOnline = true; queryShowElement('#cb-enh-inac-load-chat'); } if(data['hls_source'] === '') { playVideo = false; } if(playVideo) { const video = document.createElement('video'); video.controls = true; video.autoplay = true; video.muted = true; video.className = 'vjs-tech cb-enh-video'; video.id = 'vjs_video_3_html5_api'; video.tabIndex = -1; video.setAttribute('role', 'application'); video.poster = 'https://jpeg.live.mmcdn.com/stream?room=' + username + '&f=' + Math.random(); videoHolder.append(video); const hls_source = cutPlaylistUrl(data['hls_source']); const hls = createHLS(); hls.config.preserveManualLevelOnError = getSetting('error-quality') === false; hls.loadSource(hls_source); hls.attachMedia(video); } if(data['performer_has_fanclub']) { infoHolder2.insertAdjacentHTML( 'beforeend', '<span class="ce-loc ce-row-1" data-ce-loc="has_fanclub">Has Fanclub</span>: ' + '<span class="ce-loc" data-ce-loc="yes">Yes</span><br>' ); } else { infoHolder2.insertAdjacentHTML( 'beforeend', '<span class="ce-loc ce-row-1" data-ce-loc="has_fanclub">Has Fanclub</span>: ' + '<span class="ce-loc" data-ce-loc="no">No</span><br>' ); } if('satisfaction_score' in data) { let sc = data['satisfaction_score']; if('percent' in sc && 'up_votes' in sc && 'down_votes' in sc) { const elSpan = Object.assign(document.createElement('span'), { className: 'ce-loc ce-row-1', textContent: 'Satisfaction Score' }); elSpan.setAttribute('data-ce-loc', 'satisfaction_score'); const elSpan2 = Object.assign(document.createElement('span'), { className: 'ce-loc', textContent: 'up' }); elSpan2.setAttribute('data-ce-loc', 'up'); const elSpan3 = Object.assign(document.createElement('span'), { className: 'ce-loc', textContent: 'down' }); elSpan2.setAttribute('data-ce-loc', 'down'); infoHolder2.append( elSpan, document.createTextNode(': '), document.createTextNode(sc['percent'] + '% ('), document.createTextNode(String(sc['up_votes'])), document.createTextNode(' '), elSpan2, document.createTextNode(', '), document.createTextNode(String(sc['down_votes'])), document.createTextNode(' '), elSpan3, document.createTextNode(')'), document.createElement('br') ); } } localize(); }, onerror: function() { upperHolder.append( document.createElement('br'), document.createTextNode(getLocale('err_vid')) ); } }); xmlhttpRequest({ method: 'GET', url: 'https://cb-enh-api.improper.dev/api/room/' + username + '?lang=' + lang + '&key=' + acre3, timeout: 60*2*1000, onload: function(resp) { let data; try { data = JSON.parse(resp.responseText); } catch(e) { return; } const elLoadChatHref = document.createElement('a'); elLoadChatHref.id = 'cb-enh-inac-load-chat'; elLoadChatHref.style.display = 'none'; elLoadChatHref.className = 'ce-loc'; elLoadChatHref.dataset.ceLoc = 'try_load_chat'; elLoadChatHref.textContent = 'Click here to try to load chat.'; upperHolder2.append(elLoadChatHref, document.createElement('br')); if(isOnline) { showElement(elLoadChatHref); } elLoadChatHref.addEventListener('click', (e) => { hideElement(elLoadChatHref); e.preventDefault(); e.stopPropagation(); clearElement(videoHolder); addElement(videoHolder, 'iframe', { src: 'https://cb-enh-api2.improper.dev/embed/room/' + username + '?key=' + acre7, class: 'cb-enh-chat-frame', sandbox: 'allow-scripts allow-same-origin allow-top-navigation-by-user-activation' }); }); if(data['country']) { const elRow = createBioRow('Country'); const elVal = elRow.querySelector('.cb-enh-row-value'); elVal.innerText = data['country']; if(data['cc']) { const elFlag = document.createElement('span'); elFlag.className = 'fi fi-' + data['cc']; elFlag.style.marginLeft = '4px'; elVal.append(elFlag); } infoHolder.append(elRow); showElement(elRow); } if(data['region'] && data['region_id'] !== null) { const href = REGION_LINKS[data['region_id']]; if(href) { const elRow = createBioRow('Region'); const elLink = document.createElement('a'); elLink.innerText = data['region']; elLink.href = href; elRow.querySelector('.cb-enh-row-value').append(elLink); infoHolder.append(elRow); showElement(elRow); } } if(data['online_for'] && data['online_for'] !== '') { const elRow = createBioRow('Online for'); setBioRowValue(elRow, data['online_for']); infoHolder.append(elRow); showElement(elRow); } else if(data['last_online_f'] && data['last_online_f'] !== '') { const elRow = createBioRow('Last online'); setBioRowValue(elRow, data['last_online_f']); infoHolder.append(elRow); showElement(elRow); } const info = { 'real_name': 'Real Name', 'age': 'Age', 'birthday': 'Birthday', 'gender_f': 'Gender', 'followers_f': 'Followers', 'location': 'Location', 'languages': 'Languages', 'smoke_drink': 'Smoke / Drink', 'body_type': 'Body Type', 'body_decorations': 'Body Decorations', 'num_viewers': 'Viewers' }; Object.keys(info).forEach(function(k) { const v = info[k]; if(data[k]) { const elRow = createBioRow(v); setBioRowValue(elRow, data[k]); infoHolder.append(elRow); showElement(elRow); } }); if(data['has_schedule'] && 'schedule' in data) { const secondsOnline = data['seconds_online'] !== null ? data['seconds_online'] : 0; const elScheduleRow = createBioRow('Schedule'); elScheduleRow.querySelector('.cb-enh-row-value').style.width = '100%'; elBaseRoomContentDiv.append(elScheduleRow); showElement(elScheduleRow); initSchedule(elScheduleRow, username, secondsOnline, data.schedule); } localize(); } }); } function createBioRow(name) { const elRow = document.createElement('tr'); elRow.className = 'cb-enh-row'; elRow.innerHTML = '<td class="label"><span></span><span>:</span></td><td class="contentText cb-enh-row-value"></td>'; setBioRowName(elRow, name); return elRow; } function addBioRow(name) { const elTable = document.querySelector('.BioContents > div > table'); if(!elTable) { return; } const elRow = createBioRow(name); const elRowAbout = elTable.querySelector('div[data-testid="bio-tab-about-me-value"]')?.parentElement?.parentElement; const elRowWishlist = elTable.querySelector('div[data-testid="bio-tab-wish-list-value"]')?.parentElement?.parentElement; if(elRowAbout) { elRowAbout.parentNode.insertBefore(elRow, elRowAbout); } else if(elRowWishlist) { elRowWishlist.parentNode.insertBefore(elRow, elRowWishlist); } else { elTable.appendChild(elRow); } return elRow; } function setBioRowName(elBioRow, name) { const el = elBioRow.querySelector('td.label > span'); if(!el) { return; } const loc = name.replaceAll(' ', '_').toLowerCase(); el.innerText = name; el.classList.add('ce-loc'); el.setAttribute('data-ce-loc', loc); } function setBioRowValue(elBioRow, value) { elBioRow.querySelector('.cb-enh-row-value').innerText = value; } const acre2 = 'OudWahBXbhNmI'; function insertRoomAv(el, username) { const elAv = document.createElement('div'); elAv.className = 'cb-enh-avatar'; el.prepend(elAv); addElement(elAv, 'img', { src: 'https://cb-enh-thumb.improper.dev/av/' + username + '.jpg?key=' + acre5, alt: '', onload: 'this.parentElement.style.display="block"' }); return elAv; } function isCurrentlyUsingAnyTextInput() { if(!document.activeElement) { return false; } if( ['input', 'textarea', 'search', 'text'].includes(document.activeElement.type) ) { return true; } if(document.activeElement.hasAttribute('contenteditable')) { return true; } return false; } document.addEventListener("keydown", function(e) { if(`${e.code}` === 'KeyX' && e.ctrlKey) { if(isCurrentlyUsingAnyTextInput()) { return; } captureScreenshot(false); return; } const keyCode = e.keyCode || e.which; if(keyCode === 122) { if(!gCurrentBroadcaster) { return; } if(document.fullscreenElement) { return; } const elFullscreenBtn = document.getElementById('full-screen-icon'); if(!elFullscreenBtn) { return; } e.preventDefault(); e.stopImmediatePropagation(); elFullscreenBtn.click(); return; } } ); function captureScreenshot(displayErrors) { if(gCapturingScreenshot) { return; } if(!gCurrentBroadcaster) { if(displayErrors) { alert(getLocale('err_ss')); } return; } const rewindObj = gRewindObjs[gCurrentBroadcaster]; const elVideo = rewindObj ? getVideoHolderVideoOrRewindVideo(rewindObj.elVideoHolder) : getCurrentVideo(); if(!elVideo || !isVideoOk()) { if(displayErrors) { alert(getLocale('err_ss')); } return; } gCapturingScreenshot = true; const username = gCurrentBroadcaster; const canvas = captureVideoFrame(elVideo); gCapturingScreenshot = false; if(!canvas) { if(displayErrors) { alert(getLocale('err_ss')); } return; } const rewind = rewindObj?.rewind; const timestamp = rewind?.getPauseTime() ?? Date.now() / 1000; const date = new Date((timestamp - (rewind?.getRewindTo() || 0)) * 1000); canvas.toBlob((blob) => { const objectURL = URL.createObjectURL(blob); download({ url: objectURL, name: getFileName(username, '.png', date), onloadend: () => { setTimeout(() => { URL.revokeObjectURL(objectURL); }, 500); } }); }); } function captureVideoFrame(video, canvas = null) { if(video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) { return; } const width = video.videoWidth || video.naturalWidth || video.width; const height = video.videoHeight || video.naturalHeight || video.height; if(width === 0 || height === 0) { return; } if(!canvas) { canvas = document.createElement('canvas'); if(!canvas) { return; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if(!ctx) { return; } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); return canvas; } function isVideoOk() { const elVideo = getCurrentVideo(); if(!elVideo) { return false; } if(typeof unsafeWindow.hlsInstance === 'undefined') { return false; } if(!unsafeWindow.hlsInstance) { return false; } if(!unsafeWindow.hlsInstance.levels || unsafeWindow.hlsInstance.levels.length === 0) { return false; } return true; } function updateRecordingTimeTooltip() { const elTime = document.querySelector('#cb-enh-recording-tooltip > span'); if(!elTime) { return; } const seconds = gRecordingSession ? Math.floor((performance.now() - gRecordingSession.startedAt) / 1000) : 0; const time = formatTime(seconds); if(elTime.textContent !== time) { elTime.textContent = time; } } function updateRecordingUI() { if(gPageTitle !== null) { setPageTitle(gPageTitle); } const elRecordBtn = document.getElementById('cb-enh-video-controls-record'); elRecordBtn?.classList.toggle('cb-enh-active', gRecording); const elSpan = elRecordBtn?.querySelector('span'); if(elSpan) { const locale = gRecording ? 'stop_rec' : 'start_rec'; elSpan.innerText = getLocale(locale); elSpan.setAttribute('data-ce-loc', locale); } updateRecordingTimeTooltip(); for(const id of ['cb-enh-settings-rec-no-audio', 'cb-enh-settings-rec-no-video']) { const el = document.getElementById(id); if(el) { el.disabled = gRecording; } } } function startRecording(displayErrors) { if(gRecording || (!gRecordAudio && !gRecordVideo)) { return; } if(!gCurrentBroadcaster || !isVideoOk()) { if(displayErrors) { alert(getLocale('err_no_rec_video')); } return; } const session = { username: gCurrentBroadcaster, hls: unsafeWindow.hlsInstance, rewindObj: null, startedAt: performance.now(), failed: false }; gRecording = true; gRecordingSession = session; try { const existing = gRewindObjs[session.username]; if(existing && existing.hls !== session.hls) { rewindDestroy(existing); } rewindSetEnabled(true); session.rewindObj = gRewindObjs[session.username] || rewindInit0(); if(!session.rewindObj) { throw new Error('Rewind is unavailable for recording'); } rewindUpdateAll(); session.rewindObj.rewind.startRecordingSession(session.username, { recordAudio: gRecordAudio, recordVideo: gRecordVideo, onError: error => { console.error('Failed to save recording', error); if(gRecordingSession === session) { stopRecording(false); } if(!session.failed) { session.failed = true; alert(getLocale('err_rec_save')); } } }); } catch(error) { console.error('Failed to start recording', error); stopRecording(false); if(displayErrors) { alert(getLocale('err_no_rec_video')); } return; } updateFakeVisibility(); disableDynamicLinks(); setMulticamLinkOpenNewTab(true); updateRecordingUI(); gIntvRecordingUpdate = setInterval(() => { if(gCurrentBroadcaster !== session.username || !isVideoOk() || unsafeWindow.hlsInstance !== session.hls) { stopRecording(false); } else { updateRecordingTimeTooltip(); } }, 100); } const acre4 = 'o8R1AMIgnTLIAWOVr3kX'; function stopRecording(canceledByUser) { const session = gRecordingSession; if(!session) { return; } const saved = session.rewindObj?.rewind.stopRecordingSession(); gRecording = false; gRecordingSession = null; gIntvRecordingUpdate = clearIntervalEx(gIntvRecordingUpdate); updateFakeVisibility(); enableDynamicLinks(); setMulticamLinkOpenNewTab(false); updateRecordingUI(); rewindUpdateAll(); rewindSetEnabled(getSetting('rewind') !== false); return saved; } function setMulticamLinkOpenNewTab(enable) { queryAndRun('a[href="/multicam/"]', (el) => { el.target = enable ? '_blank' : ''; }); } function updateRoomListStatuses() { if(!window.location.pathname.startsWith('/followed-cams/')) { clearIntervalEx(gIntvUpdateFollowedList); return; } document.querySelectorAll('.RoomCard:not(.placeholder)').forEach(el => { const room = el.querySelector('a[data-room]')?.getAttribute('data-room'); if(!room) { return; } const _this = el; isRoomOnline(room, (isOnline) => { if(isOnline) { return; } getJSON('/api/biocontext/' + room + '/', function(data) { if(data['room_status'] !== 'private') { _this.querySelectorAll('.thumbnail_label.spyTokens').forEach(el => el.remove()); } if(data['room_status'] === 'public') { let label = _this.querySelector('.thumbnail_label'); if(!label) { label = _this.querySelector('.thumbnail_label_featured'); } if(label && !label.classList.contains('thumbnail_label_c_new') && !label.classList.contains('thumbnail_label_c_gaming')) { label.remove(); } return; } const elLabelContainer = _this.querySelector('.RoomCardThumbnail__labelContainer'); if(!elLabelContainer) { return; } _this.querySelectorAll('.thumbnail_label:not(.spyTokens)').forEach(el => el.remove()); _this.querySelectorAll('.thumbnail_label_featured:not(.spyTokens)').forEach(el => el.remove()); const label = document.createElement('div'); label.className = 'RoomCardThumbnailLabel thumbnail_label ce-loc'; label.textContent = getLocale('status_' + data['room_status']) || data['room_status']; const status = data['room_status']; if(status === 'private' || status === 'group' || status === 'hidden') { label.classList.add('thumbnail_label_c_private_show'); } else if(status === 'offline') { label.classList.add('thumbnail_label_offline'); } else { label.classList.add('thumbnail_label_c'); if(status === 'away') { label.classList.add('thumbnail_label_c_away'); } } elLabelContainer.prepend(label); }); }); }); } function enhanceFollowedList() { gIntvUpdateFollowedList = clearIntervalEx(gIntvUpdateFollowedList); setTimeout(function() { updateRoomListStatuses(); }, 1000); gIntvUpdateFollowedList = setInterval(function() { updateRoomListStatuses(); }, 1000 * 30); } let gGridStyle = null; let gGridMoreStyle = null; function setGridSize(width) { const gridStyleRules = `#room_list, #roomlist_root .roomlist_container ul.list, .RoomCardGrid {grid-template-columns: repeat(auto-fill,minmax(` + width + `px,1fr)) !important;}`; const style = addStyle(gridStyleRules); if(style) { if(gGridStyle) { gGridStyle.remove(); } gGridStyle = style; } } function setMoreRoomsGridSize(width) { const gridStyleRules = `.MoreRooms .list {grid-template-columns: repeat(auto-fill,minmax(` + width + `px,1fr)) !important;}`; const style = addStyle(gridStyleRules); if(style) { if(gGridMoreStyle) { gGridMoreStyle.remove(); } gGridMoreStyle = style; } } const gGridIconSvgBig = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 96 960 960" width="48"><path d="M140.001 543.307V236.001h307.306v307.306H140.001Zm0 372.692V608.693h307.306v307.306H140.001Zm372.692-372.692V236.001h307.306v307.306H512.693Zm0 372.692V608.693h307.306v307.306H512.693ZM185.385 497.924h216.539V281.385H185.385v216.539Zm372.691 0h216.539V281.385H558.076v216.539Zm0 372.691h216.539V654.076H558.076v216.539Zm-372.691 0h216.539V654.076H185.385v216.539Zm372.691-372.691Zm0 156.152Zm-156.152 0Zm0-156.152Z"/></svg>'; const gGridIconSvgMedium = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 96 960 960" width="48"><path d="M197.694 915.999q-23.529 0-40.611-17.082-17.082-17.082-17.082-40.611V293.694q0-23.529 17.082-40.611 17.082-17.082 40.611-17.082h564.612q23.529 0 40.611 17.082 17.082 17.082 17.082 40.611v564.612q0 23.529-17.082 40.611-17.082 17.082-40.611 17.082H197.694Zm0-45.384h153.845V704.461H185.385v153.845q0 5.385 3.462 8.847 3.462 3.462 8.847 3.462Zm199.229 0h166.154V704.461H396.923v166.154Zm211.538 0h153.845q5.385 0 8.847-3.462 3.462-3.462 3.462-8.847V704.461H608.461v166.154ZM185.385 659.077h166.154V492.923H185.385v166.154Zm211.538 0h166.154V492.923H396.923v166.154Zm211.538 0h166.154V492.923H608.461v166.154ZM185.385 447.539h166.154V281.385H197.694q-5.385 0-8.847 3.462-3.462 3.462-3.462 8.847v153.845Zm211.538 0h166.154V281.385H396.923v166.154Zm211.538 0h166.154V293.694q0-5.385-3.462-8.847-3.462-3.462-8.847-3.462H608.461v166.154Z"/></svg>'; const gGridIconSvgSmall = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 96 960 960" width="48"><path d="M99.232 869.076V282.924h761.536v586.152H99.232Zm45.384-405.691h133.847V328.308H144.616v135.077Zm179.23 0h133.462V328.308H323.846v135.077Zm178.846 0h133.847V328.308H502.692v135.077Zm179.23 0h133.462V328.308H681.922v135.077Zm0 180.461h133.462V508.769H681.922v135.077Zm-179.23 0h133.847V508.769H502.692v135.077Zm-178.846 0h133.462V508.769H323.846v135.077Zm-45.383-135.077H144.616v135.077h133.847V508.769Zm403.459 314.923h133.462V689.23H681.922v134.462Zm-179.23 0h133.847V689.23H502.692v134.462Zm-178.846 0h133.462V689.23H323.846v134.462Zm-179.23 0h133.847V689.23H144.616v134.462Z"/></svg>'; const gGridIconSvgSmallX = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 96 960 960" width="48"><path d="M180 876h105V771H180v105Zm165 0h105V771H345v105Zm165 0h105V771H510v105Zm165 0h105V771H675v105ZM180 381h105V276H180v105Zm0 165h105V441H180v105Zm0 165h105V606H180v105Zm165-330h105V276H345v105Zm0 165h105V441H345v105Zm0 165h105V606H345v105Zm165-330h105V276H510v105Zm0 165h105V441H510v105Zm0 165h105V606H510v105Zm165-330h105V276H675v105Zm0 165h105V441H675v105Zm0 165h105V606H675v105ZM180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Z"/></svg>'; const SVG_ICON_GRID_SIZE_1_2 = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 -960 960 960" width="48" fill="#e3e3e3"><path d="M212.31-140Q182-140 161-161q-21-21-21-51.31v-535.38Q140-778 161-799q21-21 51.31-21h535.38Q778-820 799-799q21 21 21 51.31v535.38Q820-182 799-161q-21 21-51.31 21H212.31Zm0-60H450v-560H212.31q-4.62 0-8.46 3.85-3.85 3.84-3.85 8.46v535.38q0 4.62 3.85 8.46 3.84 3.85 8.46 3.85ZM510-200h237.69q4.62 0 8.46-3.85 3.85-3.84 3.85-8.46V-480H510v280Zm0-340h250v-207.69q0-4.62-3.85-8.46-3.84-3.85-8.46-3.85H510v220Z"/></svg>'; function initGridSizeSelector() { const elTopSection = document.querySelector('.top-section'); if(!elTopSection) { return; } if(document.querySelector('.cb-enh-grid-size-selector-img:not(.cb-enh-grid-size-selector-more-img)')) { return; } const svg1 = document.createElement('div'); svg1.innerHTML = gGridIconSvgBig; const el1 = svg1.firstElementChild; el1.classList.add('cb-enh-grid-size-selector-img', 'cb-enh-first'); el1.addEventListener('click', function() { setGridSize(280); setSetting('grid-size', 280); }); const svg2 = document.createElement('div'); svg2.innerHTML = gGridIconSvgMedium; const el2 = svg2.firstElementChild; el2.classList.add('cb-enh-grid-size-selector-img'); el2.addEventListener('click', function() { setGridSize(250); setSetting('grid-size', 250); }); const svg3 = document.createElement('div'); svg3.innerHTML = gGridIconSvgSmall; const el3 = svg3.firstElementChild; el3.classList.add('cb-enh-grid-size-selector-img'); el3.addEventListener('click', function() { setGridSize(220); setSetting('grid-size', 220); }); const svg4 = document.createElement('div'); svg4.innerHTML = gGridIconSvgSmallX; const el4 = svg4.firstElementChild; el4.classList.add('cb-enh-grid-size-selector-img'); el4.addEventListener('click', function() { setGridSize(180); setSetting('grid-size', 180); }); elTopSection.append(el4, el3, el2, el1); } function initMoreGridSizeSelector() { if(gIsInMultiView) { return; } gIntvWaitMoreRooms = setInterval(function() { const elMoreRooms = document.querySelector('.MoreRooms'); if(!elMoreRooms) { return; } gIntvWaitMoreRooms = clearIntervalEx(gIntvWaitMoreRooms); const blockOuter = document.createElement('div'); blockOuter.id = 'cb-enh-more-rooms-size-selectors'; const el4 = document.createElement('div'); el4.innerHTML = gGridIconSvgSmallX; const msvg4 = el4.firstElementChild; msvg4.classList.add( 'cb-enh-grid-size-selector-img', 'cb-enh-grid-size-selector-more-img' ); msvg4.addEventListener('click', function() { setMoreRoomsGridSize(180); setSetting('grid-more-size', 180); }); const el3 = document.createElement('div'); el3.innerHTML = gGridIconSvgSmall; const msvg3 = el3.firstElementChild; msvg3.classList.add( 'cb-enh-grid-size-selector-img', 'cb-enh-grid-size-selector-more-img' ); msvg3.addEventListener('click', function() { setMoreRoomsGridSize(220); setSetting('grid-more-size', 220); }); const el2 = document.createElement('div'); el2.innerHTML = gGridIconSvgMedium; const msvg2 = el2.firstElementChild; msvg2.classList.add( 'cb-enh-grid-size-selector-img', 'cb-enh-grid-size-selector-more-img' ); msvg2.addEventListener('click', function() { setMoreRoomsGridSize(250); setSetting('grid-more-size', 250); }); const el1 = document.createElement('div'); el1.innerHTML = gGridIconSvgBig; const msvg1 = el1.firstElementChild; msvg1.classList.add( 'cb-enh-grid-size-selector-img', 'cb-enh-grid-size-selector-more-img', 'cb-enh-first' ); msvg1.addEventListener('click', function() { setMoreRoomsGridSize(280); setSetting('grid-more-size', 280); }); blockOuter.prepend(msvg4); blockOuter.prepend(msvg3); blockOuter.prepend(msvg2); blockOuter.prepend(msvg1); elMoreRooms.parentNode.insertBefore(blockOuter, elMoreRooms); }, 200); } const multiAddSVG = '<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 96 960 960" width="20"><path d="M516 661.999h51.999v-132h132v-51.998h-132v-132H516v132H384v51.998h132v132Zm-201.692 134q-27.008 0-45.657-18.65-18.65-18.65-18.65-45.658V276.309q0-27.008 18.65-45.658 18.649-18.65 45.657-18.65h455.383q27.007 0 45.657 18.65 18.65 18.65 18.65 45.658v455.382q0 27.008-18.65 45.658-18.65 18.65-45.657 18.65H314.308Zm0-51.999h455.383q4.615 0 8.462-3.846 3.846-3.847 3.846-8.463V276.309q0-4.616-3.846-8.463-3.847-3.846-8.462-3.846H314.308q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v455.382q0 4.616 3.847 8.463 3.846 3.846 8.462 3.846ZM190.309 919.997q-27.007 0-45.657-18.65-18.65-18.65-18.65-45.657V348.309h51.999V855.69q0 4.616 3.846 8.462 3.847 3.847 8.462 3.847h507.382v51.998H190.309ZM301.999 264v480-480Z"/></svg>'; function multiDoAutoHideOffline() { return getSetting('multi-auto-hide') !== false; } function multiAddUIAddButtons() { document.querySelectorAll('.RoomCard:not(.placeholder), .roomCard:not(.placeholder)').forEach(el => { if(el.getAttribute('cb-enh-multi-icon-added')) { return; } el.setAttribute('cb-enh-multi-icon-added', true); const elSpan = document.createElement('span'); elSpan.className = 'cb-enh-room-more-icon cb-enh-details-icon'; elSpan.title = getLocale('show_more_menu'); const elIcon1 = document.createElement('div'); elIcon1.innerHTML = SVG_MORE_ICON; elSpan.append(elIcon1.firstElementChild); el.append(elSpan); const elSpan2 = document.createElement('span'); elSpan2.className = 'cb-enh-add-cam-icon cb-enh-details-icon'; elSpan2.dataset.ceLoc = 'multi_add'; elSpan2.title = getLocale('multi_add'); const elIcon2 = document.createElement('div'); elIcon2.innerHTML = multiAddSVG; elSpan2.append(elIcon2.firstElementChild); el.appendChild(elSpan2); const username = el.querySelector('a[data-room]')?.getAttribute('data-room'); if(username) { el.insertAdjacentHTML('afterbegin', '<a class="cb-enh-room-preview-icon" href="/' + username + '/">' + SVG_CAMERA_ICON + '</a>'); } }); } function isFollowedDropdownOpen() { return document.querySelector('.FollowedDropdown') !== null; } function updateFollowedDropdownUI() { if(!isFollowedDropdownOpen()) { gIntvFollowedDropDownUIUpdate = clearIntervalEx(gIntvFollowedDropDownUIUpdate); return; } document.querySelectorAll('.FollowedDropdown__room').forEach(el => { if(el.getAttribute('cb-enh-multi-icon-added')) { return; } el.setAttribute('cb-enh-multi-icon-added', true); const elSpan2 = document.createElement('span'); elSpan2.className = 'cb-enh-add-cam-icon cb-enh-details-icon'; elSpan2.dataset.ceLoc = 'multi_add'; elSpan2.title = getLocale('multi_add'); const elIcon2 = document.createElement('div'); elIcon2.innerHTML = multiAddSVG; elSpan2.append(elIcon2.firstElementChild); el.appendChild(elSpan2); }); } let gRoomMoreMenuVisible = false; let gMoreRoomMenuUsername = null; function initMultiViewUINavigation() { document.addEventListener('click', function(e) { const el = e.target.closest('.cb-enh-add-cam-icon'); if(!el) { return; } e.preventDefault(); e.stopImmediatePropagation(); const username = el.closest('.RoomCard, .roomCard')?.querySelector('a[data-room]')?.dataset.room || el.closest('.FollowedDropdown__room')?.querySelector('img.FollowedDropdown__room-image')?.getAttribute('alt'); if(username) { multiAddRoomToList(username); if(isMultiViewPage()) { multiAddRoom(username); } } }, true); document.addEventListener('click', function(e) { const el = e.target.closest('.cb-enh-room-more-icon'); if(!el) { return; } e.preventDefault(); e.stopImmediatePropagation(); const elRoomCard = el.closest('.RoomCard, .roomCard'); const username = elRoomCard.querySelector('a[data-room]')?.dataset.room; const elMenu = document.getElementById('cb-enh-room-more-menu'); if(gRoomMoreMenuVisible && gMoreRoomMenuUsername == username) { setRoomMoreMenuVisible(false); } else { let country = null; gMoreRoomMenuUsername = username; elMenu.dataset.username = username; const pos = getElementOffset(el); let x = pos.left; const y = pos.top + 22; if(x > window.innerWidth - 120 - 20) { x = window.innerWidth - 120 - 20; } const elCountryFlag = elRoomCard.querySelector('.thumbnail_flag'); if(elCountryFlag) { const elCountryBlock = elCountryFlag.querySelector('.fi'); for(const cls of elCountryBlock.classList) { if(cls.startsWith('fi-') && cls.length === 5) { country = cls.slice(3); break; } } } elMenu.dataset.country = country; const showBlocklistCountry = country !== null; setRoomMoreMenuVisible(true, x, y, country); } window.getSelection().removeAllRanges(); }, true); document.addEventListener('click', (e) => { let el = e.target.closest('.cb-enh-st-blocklist-username-remove-icon'); if(el) { el.parentElement.remove(); const username = el.getAttribute('data-username'); blocklistRemove(username); blocklistSave(); return; } el = e.target.closest('.cb-enh-st-blocklist-country-remove-icon'); if(el) { el.parentElement.remove(); const country = el.getAttribute('data-country'); blocklistCountryRemove(country); blocklistCountrySave(); return; } }); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-toggle-settings'); if(!el) { return; } setSettingsOpen(true); }); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-page-overlay'); if(!el) { return; } setSettingsOpen(false); setRecordWarningModalShown(false); }); document.addEventListener('click', (e) => { const el = e.target.closest('.cb-enh-big-window-close-icon'); if(!el) { return; } setSettingsOpen(false); setRecordWarningModalShown(false); }); setIntervalRunFirst(multiAddUIAddButtons, 1000); } const STYLE_MULTI_VIEW = `body {overflow-x: hidden;}#cb-enh-multi-content {width: 100% !important;overflow-x: hidden;}#cb-enh-multi-content #cb-enh-video-controls-modal-duration-wrap {display: none;}.main-content-wrapper, .top-section {display: none !important;}#cb-enh-multi-content h1 {font-size: 26px;margin-left: 10px;float: left;margin-top: -8px;margin-bottom: 0;line-height: 48px;}#cb-enh-multi-content h2 {font-size: 20px;line-height: 20px;margin-top: 0;margin-bottom: 4px;}.darkmode #cb-enh-multi-content, #cb-enh-multi-content.cb-enh-multi-fullscreen-full {color: #f7f7f7;}.cb-enh-multi-cam {display: none;float: left;position: relative;min-width: 0;overflow: hidden;}.cb-enh-multi-cam video, .cb-enh-multi-video-iframe {border-radius: unset;display: block;width: 100%;height: 100%;top: 0;left: 0;float: left;position: absolute;max-width: unset;}.cb-enh-multi-video-iframe {border: 0;outline: 0;will-change: transform;/*pointer-events: none;*/}.cb-enh-multi-resize-snapshot {position: absolute;inset: 0;overflow: hidden;background-color: #090909;pointer-events: none;}.cb-enh-multi-resize-snapshot canvas {position: absolute;top: 0;left: 0;width: 100%;height: 100%;display: block;}.cb-enh-multi-cam img:not(.cb-enh-vcontrols-btn-img) {position: absolute;width: 100%;height: 100%;top: 0;left: 0;float: left;}.cb-enh-multi-cam .cb-enh-avatar {position: absolute;top: calc(50% - 150px/2);left: calc(50% - 150px/2);float: left;}.cb-enh-multi-drag-outline {opacity: 0;outline: 3px dashed #eb3400;outline-offset: -3px;position: absolute;left: 0;top: 0;width: 100%;height: 100%;z-index: 10;pointer-events: none;transition: 0.2s opacity;}.cb-enh-multi-cam.cb-enh-multi-cam-ondragover .cb-enh-multi-drag-outline {opacity: 1;}#cb-enh-multi-gap-drop-preview {display: none;position: fixed;outline: 3px dashed #eb3400;outline-offset: -3px;background-color: rgba(235, 52, 0, 0.15);pointer-events: none;z-index: 5001;}.cb-enh-multi-offline img, cb-enh-multi-offline video {filter: grayscale(1);}img.cb-enh-multi-offline {filter: grayscale(1);}.cb-enh-multi-offline .cb-enh-multi-cam-video-controls {display: none;}#cb-enh-multi-content input[type="button"] {text-transform: uppercase;cursor: pointer;}#cb-enh-multi-content input[type="checkbox"], #cb-enh-multi-content label {cursor: pointer;}.cb-enh-multi-info {clear: both;margin: 10px;font-size: 14px;line-height: 20px;}#cb-enh-multi-info-before {display: none;clear: both;margin: 10px;font-size: 17px;line-height: 20px;}#cb-enh-multi-share {width: 600px;outline: none;}.cb-enh-multi-link {cursor: pointer;}.cb-enh-multi-username {font-size: 20px;display: inline;opacity: 0;transition: opacity 0.25s;text-shadow: rgb(0, 0, 0) 1px 1px 0px, rgb(0, 0, 0) -1px -1px 0px, rgb(0, 0, 0) 1px -1px 0px, rgb(0, 0, 0) -1px 1px 0px;}.cb-enh-multi-username a {color: white;text-decoration: none;}.cb-enh-multi-status {display: inline;vertical-align: center;font-size: 18px;opacity: 0;transition: opacity 0.25s;pointer-events: none;color: white;text-shadow: rgb(0, 0, 0) 1px 1px 0px, rgb(0, 0, 0) -1px -1px 0px, rgb(0, 0, 0) 1px -1px 0px, rgb(0, 0, 0) -1px 1px 0px;}.cb-enh-multi-follow {background: url(https://static-assets.highwebmedia.com/images/following-off.png) no-repeat;position: absolute;top: 0;left: 2px;opacity: 0;transition: opacity 0.25s;cursor: pointer;width: 24px;height: 24px;background-size: 100% 100%;}.cb-enh-multi-follow-followed {background-image: url(https://static-assets.highwebmedia.com/images/following-hover.png);}.cb-enh-multi-follow:hover {background-image: url(https://static-assets.highwebmedia.com/images/following-off-hover.png);}.cb-enh-multi-follow-update {background-image: url(https://static-assets.highwebmedia.com/images/following-update.gif) !important;}.cb-enh-multi-username-subject-wrap {position: absolute;top: 0px;left: 26px;width: calc(100% - 28px - 28px);overflow: hidden;overflow-wrap: anywhere;}.cb-enh-multi-subject {display: none;font-size: 12px;vertical-align: center;opacity: 0;transition: opacity 0.25s;pointer-events: none;color: white;text-shadow: rgb(0, 0, 0) 1px 1px 0px, rgb(0, 0, 0) -1px -1px 0px, rgb(0, 0, 0) 1px -1px 0px, rgb(0, 0, 0) -1px 1px 0px;}.cb-enh-multi-offline .cb-enh-multi-subject {top: 25px;width: 100%;width: calc(100% - 4px - 4px);left: 4px;}body.cb-enh-multi-show-subject .cb-enh-multi-subject {display: inline;}.cb-enh-multi-close {position: absolute;top: 0;right: 7px;opacity: 0;transition: opacity 0.25s;cursor: pointer;}.cb-enh-multi-close svg path {fill: #e80000;}.cb-enh-multi-resize-handle {width: calc(8px + var(--cb-enh-resize-extra-width, 0px));position: absolute;top: 0;right: 0;height: 100%;cursor: ew-resize;background: #e0e0e0 url(https://static-assets.highwebmedia.com/tsdefaultassets/resize_arrows.svg) no-repeat center/325%;opacity: 0;transition: opacity 0.25s;}.cb-enh-multi-cam-scrollbar-edge .cb-enh-multi-resize-handle {--cb-enh-resize-extra-width: 8px;}.cb-enh-multi-resize-handle, .cb-enh-multi-drag-handle {touch-action: none;}.cb-enh-multi-drag-handle {display: none;position: absolute;top: 4px;left: 50%;transform: translateX(-50%);width: 32px;height: 32px;max-width: calc(100% - 24px);max-height: calc(100% - 8px);align-items: center;justify-content: center;border-radius: 6px;background: rgba(0, 0, 0, 0.65);cursor: grab;opacity: 0;pointer-events: none;transition: opacity 0.25s;}.cb-enh-multi-drag-handle::before {content: '';width: 14px;height: 20px;background: radial-gradient(circle, #fff 1.5px, transparent 2px) 0 0 / 7px 7px;}@media (any-pointer: coarse) {.cb-enh-multi-drag-handle {display: flex;}.cb-enh-multi-cam-hover .cb-enh-multi-resize-handle {width: calc(20px + var(--cb-enh-resize-extra-width, 0px));}}.cb-enh-multi-cam-hover .cb-enh-multi-drag-handle {opacity: 1;pointer-events: auto;}.darkmode .cb-enh-multi-resize-handle {background: #17202A url(https://static-assets.highwebmedia.com/tsdefaultassets/resize_arrows_dm.svg) no-repeat center/325%;}.cb-enh-multi-cam-hover .cb-enh-multi-resize-handle {opacity: 0.8;}.cb-enh-multi-cam-top-hover-background {height: 30px;width: 100%;position: absolute;top: 0px;left: 0px;display: flex;align-items: stretch;justify-content: space-between;pointer-events: none;visibility: visible;background: linear-gradient(180deg, rgba(0, 0, 0, 0.4) 20%, rgba(0, 0, 0, 0));}.cb-enh-multi-cam-video-controls, .cb-enh-multi-cam-top-hover-background {opacity: 0;transition: opacity 0.25s;}.cb-enh-multi-cam-hover .cb-enh-multi-username, .cb-enh-multi-cam-hover .cb-enh-multi-follow, .cb-enh-multi-cam-hover .cb-enh-multi-status, .cb-enh-multi-cam-hover .cb-enh-multi-subject, .cb-enh-multi-cam-hover .cb-enh-multi-close, .cb-enh-multi-cam-hover .cb-enh-multi-cam-video-controls, .cb-enh-multi-cam-hover .cb-enh-multi-cam-top-hover-background {opacity: 1;}.cb-enh-multi-cam-opacity-animating :is(.cb-enh-multi-username, .cb-enh-multi-follow, .cb-enh-multi-status, .cb-enh-multi-subject, .cb-enh-multi-close, .cb-enh-multi-resize-handle, .cb-enh-multi-drag-handle, .cb-enh-multi-cam-video-controls, .cb-enh-multi-cam-top-hover-background, .cb-enh-multi-drag-outline) {will-change: opacity;}#cb-enh-multi-content.cb-enh-multi-fullscreen-cam .cb-enh-multi-close,#cb-enh-multi-content.cb-enh-multi-fullscreen-cam .cb-enh-multi-drag-handle,#cb-enh-multi-content.cb-enh-multi-fullscreen-cam .cb-enh-multi-resize-handle {display: none;}.cb-enh-multi-cam-wrap, .cb-enh-multi-cam-wrap2 {height: 100%;}.cb-enh-multi-cam-wrap2 {position: relative;overflow: hidden;container-type: inline-size;container-name: multi-cam;background-color: #090909;}#cb-enh-video-controls-modal.cb-enh-multi-video-controls-modal {position: fixed;box-sizing: border-box;width: 518px;max-width: calc(100vw - 16px);max-height: calc(100vh - 16px);overflow: auto;z-index: 30;}#cb-enh-video-controls-modal.cb-enh-multi-video-controls-modal .cb-enh-arrow {display: none;}#cb-enh-multi-size-selector {position: absolute;right: 25px;}.cb-enh-multi-fullscreen {overflow: auto;}.cb-enh-multi-fullscreen #cb-enh-multi-size-selector {position: fixed;top: 2;right: 25px;z-index: 10;}.cb-enh-multi-fullscreen h1 {display: none;}.cb-enh-multi-fullscreen::-webkit-scrollbar {display: none;overflow: hidden;}.cb-enh-multi-fullscreen #cb-enh-multi-size-selector {opacity: 0;transition: 0.25s opacity;}.cb-enh-multi-fullscreen #cb-enh-multi-size-selector:hover {opacity: 1;}.cb-enh-multi-fullscreen .cb-enh-grid-size-selector-img.cb-enh-first {margin-right: 0;}.cb-enh-multi-cam-dragging {opacity: 0.7;}#cb-enh-multi-drag-img {position: fixed;display: none;top: 0;left: 0;width: 400px;height: 225px;background-color: black;pointer-events: none;z-index: 5000;opacity: 0.9;}#cb-enh-multi-drag-img canvas, #cb-enh-multi-drag-img img {width: 100%;height: 100%;}#cb-enh-multi-drag-img, #cb-enh-multi-drag-img canvas, #cb-enh-multi-drag-img img {border-radius: 10px;}.advanced-search-button-container {display: none;}.scanNext {display: none !important;}div[data-testid="messaging-entrypoint"] {display: none;}.cb-enh-vcontrols-block {flex-basis: auto !important;}#cb-enh-theater-video-controls-btn {display: inline-flex !important;}#cb-enh-theater-video-controls-btn span {display: none !important;}#cb-enh-multi-content.cb-enh-multi-fullscreen-cam #cb-enh-theater-video-controls-btn span {display: block !important;}.cb-enh-vcontrols-btn-users span {display: none !important;}#cb-enh-multi-content.cb-enh-multi-fullscreen-cam .cb-enh-vcontrols-btn-users span {display: block !important;}@container multi-cam (max-width: 520px) {.cb-enh-multi-cam .cb-enh-vcontrols-btn > span {display: none !important;}.cb-enh-multi-cam .cb-enh-vcontrols-btn,.cb-enh-multi-cam #cb-enh-pip {flex-shrink: 0;}.cb-enh-multi-cam .cb-enh-vcontrols-block.theater-overlay {column-gap: 8px !important;margin-left: 0 !important;}.cb-enh-multi-cam #cb-enh-duration {display: none !important;}}@container multi-cam (max-width: 380px) {.cb-enh-multi-cam .cb-enh-vcontrols-btn.slider,.cb-enh-multi-cam #cb-enh-pip,.cb-enh-multi-cam .cb-enh-multi-subject,.cb-enh-multi-cam .cb-enh-vcontrols-block.theater-overlay {display: none !important;}}@container multi-cam (max-width: 120px) {.cb-enh-multi-cam .cb-enh-vcontrols-block {padding: 0 !important;}}`; const gMultiViewHTML = `<div id="cb-enh-multi-content"><div id="cb-enh-multi-info-before"></div><div style="clear:both;" id="cb-enh-multi-cams"></div><div class="cb-enh-multi-info"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-multi-btn-enter-fullscreen" value="ENTER FULLSCREEN" data-ce-loc="multi_btn_enter_fscreen"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-multi-btn-enter-fullscreen-mini" value="ENTER SEMI FULLSCREEN (WITH ADDRESS BAR)" data-ce-loc="multi_btn_enter_fscreen_mini"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-multi-btn-mute-all" value="MUTE ALL" data-ce-loc="multi_btn_mute_all"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-multi-btn-remove-offline" value="REMOVE OFFLINE CAMS" data-ce-loc="multi_btn_remove_offline"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-multi-btn-remove-all" value="REMOVE ALL CAMS" data-ce-loc="multi_btn_remove_all"><input type="button" class="cb-enh-btn ce-loc" id="cb-enh-multi-btn-add-random" value="ADD RANDOM CAM" data-ce-loc="multi_btn_add_random"><div id="`+gtrrft('mspr')+`"></div><div style="height:10px"></div><h2 class="ce-loc" data-ce-loc="settings">Settings</h2>` + HTML_MULTI_VIEW_SETTINGS + `<br><br><h2 style="margin-bottom: 0" class="ce-loc" data-ce-loc="notes">Notes</h2><p><span class="ce-loc" data-ce-loc="multi_tip_can_add">You can add cams to this viewer by clicking "Add to multi cam viewer" on cam page or by using this icon in cam lists</span>: <span id="cb-enh-multi-tip-add-icon"></span></p><p class="ce-loc" data-ce-loc="multi_tip_fscreen_enter_shortcut">Press F11 to enter/exit fullscreen mode.</p><p class="ce-loc" data-ce-loc="multi_tip_fscreen_mini_shortcut">Press F10 to enter/exit semi fullscreen mode.</p><p class="ce-loc" data-ce-loc="multi_tip_fscreen_exit_shortcut">Press ESC to exit fullscreen mode.</p><p class="ce-loc" data-ce-loc="multi_tip_drag">Simply drag & drop cams to reposition them.</p><p class="ce-loc" data-ce-loc="multi_tip_resize">Use resize bar to resize cams.</p><p class="ce-loc" data-ce-loc="multi_tip_fscreen_scroll">It's possible to use scroll in fullscreen.</p><p class="ce-loc" data-ce-loc="multi_tip_auto_save">Current viewer state is being automatically saved.</p><p class="ce-loc" data-ce-loc="multi_tip_fscreen_grid_size">It's possible to set grid size in fullscreen too - hover mouse cursor on the right corner to see selectors.</p><br><h2 class="ce-loc" data-ce-loc="share">Share</h2><span class="ce-loc" data-ce-loc="multi_share">Current viewer state can be restored or shared with others (who also have installed Chaturbate Enhancer) via this link</span>:<br><input type="text" class="cb-enh-input-text" id="cb-enh-multi-share" readonly></div><div id="cb-enh-multi-drag-img"></div><div id="cb-enh-multi-gap-drop-preview" aria-hidden="true"></div></div>`; const HTML_BOTTOM_VIDEO_CONTROLS_0 = `<div style="position: relative; overflow: visible; display: flex; flex: 1 0 625px; align-items: stretch; justify-content: center; min-height: 100%;" data-paction="TheaterOverlayTabs" class="cb-enh-vcontrols-block theater-overlay"><div style="display: inline-flex; position: relative; align-items: center; user-select: none; justify-content: center; pointer-events: auto;" class="cb-enh-vcontrols-btn cb-enh-vcontrols-btn-tip hover-btn drop-shadow-container" aria-label="Send Tip"><img draggable="false" alt="Send Tip" class="cb-enh-vcontrols-btn-img" src="https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/tip-icon.svg"><span style="color: rgb(255, 255, 255); font-size: 13px; padding: 5px; display: inline-block; text-align: left;"><p style="display: inline;" class="ce-loc" data-ce-loc="send_tip">Send Tip</p></span></div><div style="display: inline-flex; position: relative; align-items: center; user-select: none; justify-content: center; pointer-events: auto;" class="cb-enh-vcontrols-btn cb-enh-vcontrols-btn-chat hover-btn drop-shadow-container" aria-label="Chat"><img draggable="false" alt="Chat" class="cb-enh-vcontrols-btn-img" src="https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/chat.svg"><span style="color: rgb(255, 255, 255); font-size: 13px; padding: 5px; display: inline-block; text-align: left;"><p style="display: inline;" class="ce-loc" data-ce-loc="chat">Chat</p></span></div><div style="display: inline-flex; position: relative; align-items: center; user-select: none; justify-content: center; pointer-events: auto;" class="cb-enh-vcontrols-btn cb-enh-vcontrols-btn-users hover-btn drop-shadow-container" aria-label="Users" data-paction-name="Users"><img draggable="false" alt="Users" class="cb-enh-vcontrols-btn-img" src="https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/users.svg"><span style="color: rgb(255, 255, 255); font-size: 13px; padding: 5px; display: inline-block; text-align: left;"><p style="display: inline;" class="ce-loc" data-ce-loc="users">Users</p></span><div class="no-drop-shadow video-controls-tooltip cb-enh-video-controls-tooltip cb-enh-video-controls-tooltip-no-fs"><p class="ce-loc" data-ce-loc="users">Users</p></div></div><div id="cb-enh-theater-video-controls-btn" class="cb-enh-vcontrols-btn hover-btn drop-shadow-container">` + SVG_VIDEO_CONTROLS + `<span><p class="ce-loc" data-ce-loc="vid_controls">Video Controls</p></span><div class="no-drop-shadow video-controls-tooltip cb-enh-video-controls-tooltip cb-enh-video-controls-tooltip-no-fs"><p class="ce-loc" data-ce-loc="vid_controls">Video Controls</p></div></div></div>`; const HTML_BOTTOM_VIDEO_CONTROLS_PAUSE_BTN = `<div style="display: inline-flex; position: relative; align-items: center; justify-content: center; min-width: 32px; user-select: none; pointer-events: auto;" class="cb-enh-vcontrols-btn cb-enh-vcontrols-btn-pause hover-btn drop-shadow-container" aria-label="Pause">` + SVG_ICON_PAUSE + `<div style="position: absolute; display: block; opacity: 0; bottom: calc(100% + 5px); left: 29.7667px; transform: translateX(-50%); border-radius: 4px; background-color: rgba(0, 0, 0, 0.92); padding: 8px 16px; text-align: center; font-size: 13px; color: rgb(255, 255, 255); width: max-content; max-width: 150px; transition: inherit; pointer-events: none; visibility: visible;" class="no-drop-shadow video-controls-tooltip cb-enh-video-controls-tooltip"><p class="ce-loc" data-ce-loc="pause">Pause</p></div></div>`; const HTML_BOTTOM_VIDEO_CONTROLS_VOLUME_BTN = `<div style="display: inline-flex; position: relative; align-items: center; justify-content: center; min-width: 32px; user-select: none; pointer-events: auto;" class="cb-enh-vcontrols-btn cb-enh-vcontrols-btn-vol hover-btn drop-shadow-container" aria-label="Unmute"><img class="cb-enh-vcontrols-btn-img" src="https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/volume-mute.svg" draggable="false" alt="Unmute"><div style="position: absolute; display: block; opacity: 0; bottom: calc(100% + 5px); left: 29.7667px; transform: translateX(-50%); border-radius: 4px; background-color: rgba(0, 0, 0, 0.92); padding: 8px 16px; text-align: center; font-size: 13px; color: rgb(255, 255, 255); width: max-content; max-width: 150px; transition: inherit; pointer-events: none; visibility: visible;" class="no-drop-shadow video-controls-tooltip cb-enh-video-controls-tooltip"><p class="ce-loc" data-ce-loc="unmute">Unmute</p></div></div>`; const HTML_BOTTOM_VIDEO_CONTROLS_VOLUME_SLIDER = `<div style="height: 12px; width: 62px; position: relative; overflow: visible; cursor: pointer; display: inline-flex; margin-block: auto; margin-left: 6px; margin-right: 6px; pointer-events: auto;" class="cb-enh-vcontrols-btn slider hover-btn drop-shadow-container" aria-label="Volume Slider"><div class="cb-enh-vcontrols-slider-part cb-enh-vcontrols-slider-base-bg" style="position: absolute; box-sizing: border-box; background-color: rgba(255, 255, 255, 0.2); top: 4px; height: 4px; width: 100%;"></div><div class="cb-enh-vcontrols-slider-part cb-enh-vcontrols-slider-base" style="position: absolute; box-sizing: border-box; background-color: rgb(255, 255, 255); top: 4px; height: 4px; width: 0px;"></div><div class="cb-enh-vcontrols-slider-part cb-enh-vcontrols-slider-dot" style="position: absolute; top: 0px; background-color: rgb(255, 255, 255); border-radius: 50%; height: 12px; width: 12px; cursor: grab; left: 0px;"></div><div style="position: absolute; display: block; opacity: 0; bottom: calc(100% + 19px); left: 50%; transform: translateX(-50%); border-radius: 4px; background-color: rgba(0, 0, 0, 0.92); padding: 8px 16px; text-align: center; font-size: 13px; color: rgb(255, 255, 255); width: max-content; max-width: 150px; transition: inherit; pointer-events: none; visibility: visible;" class="no-drop-shadow video-controls-tooltip cb-enh-video-controls-tooltip"><p class="ce-loc" data-ce-loc="vol">Volume</p></div></div>`; const HTML_BOTTOM_VIDEO_CONTROLS = `<div style="height: 35px; width: 100%; position: absolute; bottom: 0px; left: 0px; z-index: 3; display: flex; align-items: stretch; justify-content: space-between; pointer-events: none; transition: 500ms; opacity: 1; visibility: visible;" class="theater-video-controls dark-gradient-bg"><div class="cb-enh-vcontrols-block cb-enh-vcontrols-block-left" style="position: relative; overflow: visible; display: flex; flex-grow: 0; flex-shrink: 0; align-items: stretch; justify-content: flex-start; min-height: 100%; padding-left: 4px;">` + HTML_BOTTOM_VIDEO_CONTROLS_VOLUME_BTN + `` + HTML_BOTTOM_VIDEO_CONTROLS_VOLUME_SLIDER + `</div>` + HTML_BOTTOM_VIDEO_CONTROLS_0 + `<div class="cb-enh-vcontrols-block cb-enh-vcontrols-block-right" style="position: relative; overflow: visible; display: flex; flex: 0 1 218px; column-gap: 0px; padding-right: 8px; align-items: stretch; justify-content: flex-end; min-height: 100%;"><div class="hover-btn drop-shadow-container" id="cb-enh-pip">` + SVG_PIP_ICON + `<div class="no-drop-shadow video-controls-tooltip"><p class="ce-loc" data-ce-loc="pip">Picture in Picture</p></div></div><div style="display: inline-flex; position: relative; align-items: center; justify-content: center; min-width: 32px; user-select: none; pointer-events: auto;" class="cb-enh-vcontrols-btn cb-enh-vcontrols-btn-fs hover-btn drop-shadow-container" aria-label="Fullscreen"><img src="https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/fullscreen.svg" draggable="false" alt="Fullscreen" class="cb-enh-vcontrols-btn-img animated-icon scale-big"><div style="position: absolute; display: block; opacity: 0; bottom: calc(100% + 5px); left: -5.00003px; transform: translateX(-50%); border-radius: 4px; background-color: rgba(0, 0, 0, 0.92); padding: 8px 16px; text-align: center; font-size: 13px; color: rgb(255, 255, 255); width: max-content; max-width: 150px; transition: inherit; pointer-events: none; visibility: visible;" class="no-drop-shadow video-controls-tooltip"><p class="ce-loc" data-ce-loc="fs">Fullscreen</p></div></div></div></div>`; const multiViewCamHTML = `<div class="cb-enh-multi-cam" data-cb-enh-username="{{username}}" draggable="true"><div class="cb-enh-multi-drag-outline"></div><div class="cb-enh-multi-cam-wrap"><div class="cb-enh-multi-cam-wrap2"><div class="cb-enh-multi-cam-img-wrap"></div><div class="cb-enh-multi-cam-video-wrap"></div><div class="cb-enh-multi-cam-video-controls">` + HTML_BOTTOM_VIDEO_CONTROLS + `</div><div class="cb-enh-multi-cam-top-hover-background"></div><div class="cb-enh-multi-follow noselect"></div><div class="cb-enh-multi-username-subject-wrap"><div class="cb-enh-multi-username"><a href="/{{username}}/" target="_blank" rel="noopener">{{username}}</a></div><div class="cb-enh-multi-status noselect"></div><div class="cb-enh-multi-subject noselect"></div></div><div class="cb-enh-multi-resize-handle noselect"></div><div class="cb-enh-multi-close noselect">{{SVG_CLOSE_ICON}}</div><div class="cb-enh-multi-drag-handle noselect" title="Drag cam"></div></div></div></div>`; const gMultiIframeMode = true; let gMultiRooms = []; let gMultiRoomsData = []; let gMultiInMiniFullscreen = false; let gIntvMultiUpdateRooms = null; let gMultiIsDragging = false; let gMultiIsResizing = false; let gMultiResizeCurrentCam = null; let gMultiResizeStartPos = null; let gMultiDropReplace = true; let gMultiHoverBlock = null; let gMultiCustomGridActive = false; let gMultiPrepareGridResize = null; let gMultiUnmuteOnHover = false; let gMultiHoverForever = false; const gMultiHoverTime = 2500; function multiUpdateSettings() { queryAndRun('.cb-enh-checkbox[data-id="cb-enh-multi-btn-auto-remove"]', el => { el.checked = getSetting('multi-auto-remove'); }); queryAndRun('.cb-enh-checkbox[data-id="cb-enh-multi-btn-auto-hide"]', el => { el.checked = multiDoAutoHideOffline(); }); queryAndRun('.cb-enh-checkbox[data-id="cb-enh-multi-btn-auto-hide-priv"]', el => { el.checked = getSetting('multi-auto-hide-priv'); }); queryAndRun('.cb-enh-checkbox[data-id="cb-enh-multi-unmute-hover"]', el => { el.checked = getSetting('multi-unmute-hover'); }); queryAndRun('.cb-enh-checkbox[data-id="cb-enh-multi-align-right"]', el => { el.checked = getSetting('multi-align-right'); }); let showSubject = getSetting('multi-show-subject'); if(showSubject === null) { showSubject = true; } queryAndRun('.cb-enh-checkbox[data-id="cb-enh-multi-show-subject"]', el => { el.checked = showSubject; }); queryAndRun('.cb-enh-multi-max-quality', el => { el.value = multiGetMaxHeight(); }); const randomGenders = multiGetRandomGenders(); queryAndRun('.cb-enh-multi-checkbox-random-gender', el => { el.checked = false; }); for(const v of randomGenders) { queryAndRun('.cb-enh-multi-checkbox-random-gender[data-gender="' + v + '"]', el => { el.checked = true; }); } } function setCamBlockOrder(elDragBlock, order) { const username = elDragBlock.getAttribute('data-cb-enh-username'); const oldIndex = gMultiRooms.indexOf(username); if(oldIndex === -1) { return; } gMultiRooms.splice(oldIndex, 1); order = Math.max(0, Math.min(order, gMultiRooms.length)); gMultiRooms.splice(order, 0, username); multiUpdateCamOrder(); } function multiUpdateCamOrder() { gMultiRooms.forEach((username, index) => { const elBlock = document.querySelector('.cb-enh-multi-cam[data-cb-enh-username="' + username + '"]'); if(elBlock) { elBlock.style.order = index; } }); multiRefreshCustomGrid(); } function updateSliderPosition(e, el) { const rect = el.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width )); const muted = position == 0; if(gIsInMultiView) { const elBlock = el.closest('.cb-enh-multi-cam'); if(!gMultiIframeMode) { const elVideo = elBlock.querySelector('video'); elVideo.volume = position; elVideo.muted = muted; } else { multiSetIframeVolume(elBlock, muted, position); } } else { const elBlock = el.closest('.cb-enh-rewinded'); if(elBlock) { const elVideo = getVideoHolderRewindVideo(elBlock); elVideo.volume = position; elVideo.muted = muted; updateVideoUI(elBlock); } } e.preventDefault(); e.stopImmediatePropagation(); } function initMultiView() { gIsInMultiView = true; gMultiCustomGridActive = parseInt(getSetting('multi-grid-size')) === gMultiCustomGridSize && multiLoadRoomsSettings().layoutMode !== 'custom'; document.body.classList.add('cb-enh-multi'); clearAllTimeouts(); clearAllIntervals(); if(typeof URLSearchParams !== 'undefined' && window?.location?.search) { const params = new URLSearchParams(window?.location?.search); if(params) { let rooms = params.get('rooms'); if(rooms) { rooms = rooms.split(','); multiClearRoomList(); rooms.forEach(function(room) { if(!room.match(/^[a-zA-Z0-9_]+$/)) { return; } multiAddRoomToList(room); }); } } } addStyle(STYLE_MULTI_VIEW); document.querySelector('#main').insertAdjacentHTML('beforeend', gMultiViewHTML); document.addEventListener('click', (e) => { let el = e.target.closest('.cb-enh-multi-close'); if(el) { const username = el.closest('.cb-enh-multi-cam').getAttribute('data-cb-enh-username'); multiRemoveRoom(username); return; } el = e.target.closest('.cb-enh-vcontrols-btn-fs'); if(el) { if(document.fullscreenElement) { document.exitFullscreen(); return; } setVideoControlsVisible(false); el.closest('.cb-enh-multi-cam-wrap2').requestFullscreen(); return; } el = e.target.closest('.cb-enh-vcontrols-btn-chat'); if(el) { const elBlock = el.closest('.cb-enh-multi-cam'); const elIframe = elBlock.querySelector('.cb-enh-multi-video-iframe'); sendIframeEvent(elIframe, 'toggle-chat'); setVideoControlsVisible(false); e.preventDefault(); e.stopImmediatePropagation(); return; } el = e.target.closest('.cb-enh-vcontrols-btn-tip'); if(el) { const elBlock = el.closest('.cb-enh-multi-cam'); const elIframe = elBlock.querySelector('.cb-enh-multi-video-iframe'); sendIframeEvent(elIframe, 'toggle-tip'); setVideoControlsVisible(false); e.preventDefault(); e.stopImmediatePropagation(); return; } el = e.target.closest('.cb-enh-vcontrols-btn-users'); if(el) { const elBlock = el.closest('.cb-enh-multi-cam'); const elIframe = elBlock.querySelector('.cb-enh-multi-video-iframe'); sendIframeEvent(elIframe, 'toggle-users'); setVideoControlsVisible(false); e.preventDefault(); e.stopImmediatePropagation(); return; } }); document.addEventListener('click', (e) => { const el = e.target.closest('.cb-enh-multi-follow'); if(!el) { return; } if(el.classList.contains('cb-enh-multi-follow-update')) { return; } const username = el.closest('.cb-enh-multi-cam').getAttribute('data-cb-enh-username'); el.classList.add('cb-enh-multi-follow-update') const isFollowing = el.classList.contains('cb-enh-multi-follow-followed'); function callbackOk(isFollowing) { el.classList.remove('cb-enh-multi-follow-update'); el.classList.toggle('cb-enh-multi-follow-followed', isFollowing); } function callbackErr() { el.classList.remove('cb-enh-multi-follow-update'); } setFollow(username, !isFollowing, callbackOk, callbackErr); }); let multiResizeFrame = null; let multiResizeFirstCam = false; const multiResizeCurrentSnapshot = {snapshot: null, timer: null, failed: false}; const multiResizeNextSnapshot = {snapshot: null, timer: null, failed: false}; let multiGridResizeSnapshots = []; let multiGridResizeFrame = null; let multiGridResizeTimer = null; function restoreCamResizeIframe(snapshot) { if(!snapshot?.iframeSize) { return; } const size = snapshot.iframeSize; snapshot.iframeSize = null; size.iframe.style.setProperty('width', size.width, size.widthPriority); size.iframe.style.setProperty('height', size.height, size.heightPriority); } function holdCamResizeIframe(snapshot, target, bounds = null) { if(snapshot.iframeSize) { return; } const iframe = target.querySelector('.cb-enh-multi-video-iframe'); if(!iframe || iframe.contentDocument !== snapshot.video.ownerDocument) { return; } bounds ??= iframe.getBoundingClientRect(); if(bounds.width <= 0 || bounds.height <= 0) { return; } snapshot.iframeSize = { iframe: iframe, width: iframe.style.width, height: iframe.style.height, widthPriority: iframe.style.getPropertyPriority('width'), heightPriority: iframe.style.getPropertyPriority('height') }; iframe.style.setProperty('width', bounds.width + 'px', snapshot.iframeSize.widthPriority); iframe.style.setProperty('height', bounds.height + 'px', snapshot.iframeSize.heightPriority); } function syncCamResizeSnapshotTransform(canvas, style) { canvas.style.transform = style.transform; const origin = style.transformOrigin.split(' '); const width = parseFloat(style.width); const height = parseFloat(style.height); canvas.style.transformOrigin = width > 0 && height > 0 ? (parseFloat(origin[0]) / width * 100) + '% ' + (parseFloat(origin[1]) / height * 100) + '%' : style.transformOrigin; } function clearCamResizeSnapshot(state = multiResizeNextSnapshot) { if(state.timer !== null) { clearTimeout(state.timer); state.timer = null; } if(state.snapshot) { const snapshot = state.snapshot; state.snapshot = null; restoreCamResizeIframe(snapshot); if(snapshot.frame !== null) { if(snapshot.useVideoFrames) { snapshot.video.cancelVideoFrameCallback(snapshot.frame); } else { cancelAnimationFrame(snapshot.frame); } } snapshot.overlay.remove(); snapshot.canvas.width = 0; snapshot.canvas.height = 0; } } function deferClearCamResizeSnapshot(state) { if(!state.snapshot) { return; } if(state.timer !== null) { clearTimeout(state.timer); } state.timer = setTimeout(() => clearCamResizeSnapshot(state), 150); } function scheduleCamResizeSnapshot(state, snapshot) { const callback = () => updateCamResizeSnapshot(state, snapshot); snapshot.frame = snapshot.useVideoFrames ? snapshot.video.requestVideoFrameCallback(callback) : requestAnimationFrame(callback); } function updateCamResizeSnapshot(state, snapshot) { if(state.snapshot !== snapshot) { return; } snapshot.frame = null; if(!snapshot.overlay.isConnected || !snapshot.video.isConnected) { clearCamResizeSnapshot(state); return; } try { const video = snapshot.video; const canvas = snapshot.canvas; if(canvas.style.transform !== 'none') { const style = video.ownerDocument.defaultView.getComputedStyle(video); syncCamResizeSnapshotTransform(canvas, style); } if(video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0 && video.videoHeight > 0) { if(canvas.width !== video.videoWidth) { canvas.width = video.videoWidth; } if(canvas.height !== video.videoHeight) { canvas.height = video.videoHeight; } snapshot.context.drawImage(video, 0, 0, canvas.width, canvas.height); } scheduleCamResizeSnapshot(state, snapshot); } catch(e) { state.failed = true; clearCamResizeSnapshot(state); } } function showCamResizeSnapshot(state = multiResizeNextSnapshot, target = null) { if(state.failed) { return; } if(state.snapshot && (!state.snapshot.overlay.isConnected || !state.snapshot.video.isConnected)) { clearCamResizeSnapshot(state); } if(!state.snapshot) { try { let video = target ? getVideoHolderVideo(target) : null; if(!target) { const cams = [...gMultiResizeCurrentCam.parentElement.children] .filter(el => el.classList.contains('cb-enh-multi-cam') && el.offsetWidth > 0 && el.offsetHeight > 0) .sort((a, b) => Number(a.style.order) - Number(b.style.order)); if(cams[0] !== gMultiResizeCurrentCam) { return; } for(const cam of cams.slice(1)) { const candidate = getVideoHolderVideo(cam); if(candidate && candidate.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && candidate.videoWidth > 0 && candidate.videoHeight > 0) { target = cam; video = candidate; break; } } } const holder = target?.querySelector('.cb-enh-multi-cam-video-wrap'); if(!holder || !video) { return; } const canvas = captureVideoFrame(video); if(!canvas) { return; } const style = video.ownerDocument.defaultView.getComputedStyle(video); canvas.style.objectFit = style.objectFit; canvas.style.objectPosition = style.objectPosition; syncCamResizeSnapshotTransform(canvas, style); canvas.style.filter = style.filter; const overlay = document.createElement('div'); overlay.className = 'cb-enh-multi-resize-snapshot'; overlay.setAttribute('aria-hidden', 'true'); overlay.append(canvas); holder.append(overlay); state.snapshot = { overlay: overlay, canvas: canvas, video: video, context: canvas.getContext('2d'), frame: null, iframeSize: null, useVideoFrames: typeof video.requestVideoFrameCallback === 'function' && typeof video.cancelVideoFrameCallback === 'function' }; scheduleCamResizeSnapshot(state, state.snapshot); if(state === multiResizeCurrentSnapshot && gMultiIsResizing) { holdCamResizeIframe(state.snapshot, target); } } catch(e) { state.failed = true; clearCamResizeSnapshot(state); return; } } if(state === multiResizeNextSnapshot) { deferClearCamResizeSnapshot(state); } } function clearGridResizeSnapshots() { if(multiGridResizeFrame !== null) { cancelAnimationFrame(multiGridResizeFrame); multiGridResizeFrame = null; } if(multiGridResizeTimer !== null) { clearTimeout(multiGridResizeTimer); multiGridResizeTimer = null; } for(const state of multiGridResizeSnapshots) { clearCamResizeSnapshot(state); } multiGridResizeSnapshots = []; } gMultiPrepareGridResize = (targets = null) => { if(gMultiIsResizing) { return; } clearGridResizeSnapshots(); clearCamResizeSnapshot(); clearCamResizeSnapshot(multiResizeCurrentSnapshot); const cams = [...(targets ?? document.querySelectorAll('.cb-enh-multi-cam'))] .filter(cam => cam.offsetWidth > 0 && cam.offsetHeight > 0) .map(cam => ({cam: cam, bounds: cam.querySelector('.cb-enh-multi-video-iframe')?.getBoundingClientRect()})); for(const item of cams) { const state = {snapshot: null, timer: null, failed: false}; showCamResizeSnapshot(state, item.cam); if(!state.snapshot) { continue; } try { holdCamResizeIframe(state.snapshot, item.cam, item.bounds); multiGridResizeSnapshots.push(state); } catch(e) { clearCamResizeSnapshot(state); } } if(multiGridResizeSnapshots.length === 0) { return; } return () => { multiGridResizeFrame = requestAnimationFrame(() => { multiGridResizeFrame = requestAnimationFrame(() => { multiGridResizeFrame = null; for(const state of multiGridResizeSnapshots) { restoreCamResizeIframe(state.snapshot); } multiGridResizeTimer = setTimeout(clearGridResizeSnapshots, 150); }); }); }; }; function applyCamResize() { if(!gMultiIsResizing || !gMultiResizeCurrentCam?.isConnected) { return; } const bodyWidth = document.body.clientWidth; if(bodyWidth <= 0 || cursorX > bodyWidth) { return; } let width = cursorX - gMultiResizeStartPos; if(getSetting('multi-align-right')) { width = gMultiResizeStartPos - cursorX; } const columnEnd = Math.min(gMultiGridColumnCount, Math.max(gMultiMinColumnSpan, Math.round((width / bodyWidth) * gMultiGridColumnCount))); const widthReal = (columnEnd / gMultiGridColumnCount) * bodyWidth; const height = widthReal * 9/16; const rowCount = Math.round(height); const gridColumn = 'span ' + columnEnd; const gridRow = 'span ' + rowCount; const currentSpan = multiGetCamColumnSpan(gMultiResizeCurrentCam); showCamResizeSnapshot(multiResizeCurrentSnapshot, gMultiResizeCurrentCam); if(multiResizeFirstCam && currentSpan !== null && columnEnd < currentSpan) { showCamResizeSnapshot(); } else if(currentSpan !== null && columnEnd > currentSpan) { clearCamResizeSnapshot(); } if(gMultiResizeCurrentCam.style.gridColumn !== gridColumn) { gMultiResizeCurrentCam.style.gridColumn = gridColumn; } if(gMultiResizeCurrentCam.style.gridRow !== gridRow) { gMultiResizeCurrentCam.style.gridRow = gridRow; } } function resizeCam(e) { cursorX = e.pageX; cursorY = e.pageY; if(multiResizeFrame !== null) { return; } multiResizeFrame = requestAnimationFrame(() => { multiResizeFrame = null; applyCamResize(); }); } let multiGesturePointerId = null; let multiGestureHandle = null; let multiPendingDrag = null; function releaseMultiGesture() { const handle = multiGestureHandle; const pointerId = multiGesturePointerId; multiGesturePointerId = null; multiGestureHandle = null; multiPendingDrag = null; dragIsPointerDown = false; if(handle && pointerId !== null && handle.hasPointerCapture(pointerId)) { handle.releasePointerCapture(pointerId); } } document.addEventListener('pointerdown', (e) => { if(!e.isPrimary || e.button !== 0 || multiGesturePointerId !== null || gMultiIsDragging || gMultiIsResizing) { return; } if(e.pointerType === 'touch') { cursorX = e.pageX; cursorY = e.pageY; updateCamHover(); } const dragHandle = e.target.closest?.('.cb-enh-multi-drag-handle'); const el = e.target.closest?.('.cb-enh-multi-resize-handle'); if(!el && !dragHandle) { return; } e.preventDefault(); e.stopPropagation(); multiGesturePointerId = e.pointerId; multiGestureHandle = el || dragHandle; multiGestureHandle.setPointerCapture(e.pointerId); cursorX = e.pageX; cursorY = e.pageY; if(dragHandle) { multiPendingDrag = {cam: dragHandle.closest('.cb-enh-multi-cam'), x: e.clientX, y: e.clientY}; return; } clearGridResizeSnapshots(); gMultiIsResizing = true; gMultiResizeCurrentCam = el.closest('.cb-enh-multi-cam'); clearCamResizeSnapshot(); clearCamResizeSnapshot(multiResizeCurrentSnapshot); multiResizeNextSnapshot.failed = false; multiResizeCurrentSnapshot.failed = false; multiResizeFirstCam = false; if(isFirefox()) { const cams = [...gMultiResizeCurrentCam.parentElement.children] .filter(el => el.classList.contains('cb-enh-multi-cam') && el.offsetWidth > 0 && el.offsetHeight > 0) .sort((a, b) => Number(a.style.order) - Number(b.style.order)); multiResizeFirstCam = cams[0] === gMultiResizeCurrentCam; } showCamResizeSnapshot(multiResizeCurrentSnapshot, gMultiResizeCurrentCam); gMultiCustomGridActive = false; multiResetCamPositions(); const width = gMultiResizeCurrentCam.offsetWidth; gMultiResizeStartPos = getSetting('multi-align-right') ? e.pageX + width : e.pageX - width; document.body.style.cursor = 'ew-resize'; if(gMultiIframeMode) { queryAndRun('.cb-enh-multi-video-iframe', el => { el.style.pointerEvents = 'none'; }); } }); function finishCamResize(e = null) { if(!gMultiIsResizing) { return; } if(multiResizeFrame !== null) { cancelAnimationFrame(multiResizeFrame); multiResizeFrame = null; } if(e) { cursorX = e.pageX; cursorY = e.pageY; applyCamResize(); } gMultiIsResizing = false; restoreCamResizeIframe(multiResizeCurrentSnapshot.snapshot); deferClearCamResizeSnapshot(multiResizeCurrentSnapshot); multiSaveCamSizes(); releaseMultiGesture(); scheduleMultiPointerUpdate(); document.body.style.cursor = 'unset'; if(gMultiIframeMode) { queryAndRun('.cb-enh-multi-video-iframe', el => { el.style.pointerEvents = 'auto'; }); } } document.addEventListener('focusin', (e) => { if(!e.target.matches('#cb-enh-multi-share')) { return; } e.target.setSelectionRange(0, e.target.value.length); }); addEventListener('storage', function(e) { if(e.key !== 'cb-enh-multicam') { return; } const data = multiLoadRoomsSettings(); if(!gMultiIsDragging && !gMultiIsResizing) { gMultiCustomGridActive = parseInt(getSetting('multi-grid-size')) === gMultiCustomGridSize && data.layoutMode !== 'custom'; } multiUpdateRooms(); if(!gMultiIsDragging && !gMultiIsResizing) { const bodyWidth = document.body.clientWidth; document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { multiRestoreCamSize(el, data, bodyWidth); }); } }); document.addEventListener('volumechange', (e) => { const elBlock = e.target.closest('.cb-enh-multi-cam'); if(elBlock) { updateVideoUI(elBlock); } }, true) multiUpdateRooms(); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-multi-btn-remove-offline'); if(!el) { return; } multiRemoveOfflineRooms(); window.scrollTo(0, 0); }); document.addEventListener('click', (e) => { const el1 = e.target.closest('#cb-enh-multi-btn-remove-all'); if(el1) { multiRemoveAllRooms(); return; } const el2 = e.target.closest('#cb-enh-multi-btn-add-random'); if(el2) { const genders = multiGetRandomGenders(); getRandomRoom((data) => { if(gMultiRooms.indexOf(data.name) === -1) { multiAddRoom(data.name); } else { document.getElementById('cb-enh-multi-btn-add-random').click(); } }, genders); return; } }); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-multi-btn-mute-all'); if(!el) { return; } multiMuteAllVideos(); }); document.addEventListener('change', (e) => { const el = e.target.closest('.cb-enh-multi-max-quality'); if(!el) { return; } setSetting('multi-max-quality', parseInt(el.value)); multiUpdateAllVideosQuality(); multiUpdateSettings(); }); multiUpdateSettings(); let showSubject = getSetting('multi-show-subject'); if(showSubject === null) { showSubject = true; } document.body.classList.toggle('cb-enh-multi-show-subject', showSubject); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-multi-btn-enter-fullscreen'); if(!el) { return; } if(!document.fullscreenElement) { multiEnterFullscreen(); } else { document.exitFullscreen(); } }); document.addEventListener('click', (e) => { const el = e.target.closest('#cb-enh-multi-btn-enter-fullscreen-mini'); if(!el) { return; } multiToggleFullscreenMini(); }); window.addEventListener('keydown', function(e) { const keyCode = e.keyCode || e.which; if(keyCode === 122) { multiEnterFullscreen(); e.preventDefault(); e.stopImmediatePropagation(); return; } if(keyCode === 121 && !e.repeat) { multiToggleFullscreenMini(); e.preventDefault(); e.stopImmediatePropagation(); return; } if(keyCode === 27 && !e.repeat) { if(multiGesturePointerId !== null) { cancelMultiGesture(); e.preventDefault(); e.stopImmediatePropagation(); return; } if(gMultiIsDragging) { multiOnDragEnd(true) e.preventDefault(); e.stopImmediatePropagation(); return; } if(gMultiInMiniFullscreen) { multiToggleFullscreenMini(); e.preventDefault(); e.stopImmediatePropagation(); return; } } }); multiUpdateShareURL(); gIntvMultiUpdateRooms = clearIntervalEx(gIntvMultiUpdateRooms); gIntvMultiUpdateRooms = setInterval(multiUpdateRoomStatuses, 1000 * 30); document.getElementById('cb-enh-multi-tip-add-icon').insertAdjacentHTML('beforeend', multiAddSVG); let intvClearHover = null; const multiOpacityTimers = new WeakMap(); function setCamOpacityState(cam, className, enabled) { if(cam.classList.contains(className) === enabled) { return; } clearTimeout(multiOpacityTimers.get(cam)); cam.classList.add('cb-enh-multi-cam-opacity-animating'); cam.classList.toggle(className, enabled); multiOpacityTimers.set(cam, setTimeout(() => { cam.classList.remove('cb-enh-multi-cam-opacity-animating'); multiOpacityTimers.delete(cam); }, 275)); } function clearHover() { if(gMultiHoverBlock && !gMultiHoverForever) { setCamOpacityState(gMultiHoverBlock, 'cb-enh-multi-cam-hover', false); } } window.addEventListener('message', e => { if(e.origin !== location.origin) { return; } if(e.data.type !== 'cb-enh-multi-iframe') { return; } const iframe = [...document.querySelectorAll('.cb-enh-multi-cam iframe')] .find(iframe => iframe.contentWindow === e.source); if(!iframe) { return; } if(e.data.event === 'video-found') { const elBlock = iframe.closest('.cb-enh-multi-cam'); if(elBlock) { const elVideo = getVideoHolderVideo(elBlock); setFreezoomEnabled(elBlock, true); elVideo.draggable = true; elVideo.addEventListener('dragstart', e => { e.preventDefault(); e.stopImmediatePropagation(); multiDragStart(elBlock); }); elVideo.addEventListener('dblclick', () => { if(gFreezoomInstances.get(elVideo).getTransform().zoom != 1) {e e.stopImmediatePropagation(); e.preventDefault(); return; } multiToggleCamFullscreen(iframe); }); } return; } if(e.data.event === 'update-subject') { const elBlock = iframe.closest('.cb-enh-multi-cam'); if(elBlock) { const username = elBlock.getAttribute('data-cb-enh-username'); elBlock.querySelector('.cb-enh-multi-subject').textContent = filterSubject(username, e.data.data.subject); } return; } if(e.data.event === 'update-status') { const elBlock = iframe.closest('.cb-enh-multi-cam'); if(elBlock) { multiUpdateRoomStatus(elBlock, e.data.data.status); } return; } }); let cursorX = 0; let cursorY = 0; let elDragBlock = null; let elDragHoverBlock = null let gapDropTarget = null; let gapGeometry = null; const elMultiCams = document.getElementById('cb-enh-multi-cams'); let dragScrollX = 0; let dragScrollY = 0; let dragImageWidth = 0; let dragImageHeight = 0; const elMultiDragImg = document.getElementById('cb-enh-multi-drag-img'); let multiPointerFrame = null; let multiIframePointerBounds = null; let multiDragScrollTime = null; let multiDragScrollRemainder = 0; let multiDragPointerInside = false; let dragIsPointerDown = false; function invalidateGapGeometry() { if(!gMultiIsDragging) { return; } gapGeometry = null; scheduleMultiPointerUpdate(); } const gapResizeObserver = new ResizeObserver(invalidateGapGeometry); const gapLayoutObserver = new MutationObserver(mutations => { for(const mutation of mutations) { if(mutation.type !== 'childList') { continue; } for(const block of mutation.removedNodes) { if(block.nodeType === Node.ELEMENT_NODE) { gapResizeObserver.unobserve(block); } } for(const block of mutation.addedNodes) { observeGapCam(block); } } invalidateGapGeometry(); }); function observeGapCam(block) { if(block.nodeType !== Node.ELEMENT_NODE || !block.classList.contains('cb-enh-multi-cam')) { return; } gapResizeObserver.observe(block); gapLayoutObserver.observe(block, {attributes: true, attributeFilter: ['style']}); } function startGapGeometryCache() { gapGeometry = null; gapResizeObserver.observe(elMultiCams); gapLayoutObserver.observe(elMultiCams, { childList: true, attributes: true, attributeFilter: ['style', 'class', 'dir'] }); for(const ancestor of [document.body, document.documentElement]) { gapLayoutObserver.observe(ancestor, {attributes: true, attributeFilter: ['style', 'class', 'dir']}); } for(const block of elMultiCams.children) { observeGapCam(block); } } function clearGapGeometryCache() { gapResizeObserver.disconnect(); gapLayoutObserver.disconnect(); gapGeometry = null; } function updateGapDropTarget() { if(!gMultiIsDragging) { return; } gapDropTarget = null; const preview = document.getElementById('cb-enh-multi-gap-drop-preview'); if(elDragHoverBlock || document.fullscreenElement?.classList.contains('cb-enh-multi-cam-wrap2')) { hideElement(preview); return; } const bounds = elMultiCams.getBoundingClientRect(); const clientX = cursorX - window.scrollX; const clientY = cursorY - window.scrollY; if(bounds.width <= 0 || clientX < bounds.left || clientX >= bounds.right || clientY < bounds.top || clientY >= bounds.bottom) { hideElement(preview); return; } if(!gapGeometry || gapGeometry.width !== bounds.width || gapGeometry.height !== bounds.height) { gapGeometry = multiMeasureGapGeometry(elMultiCams, bounds); } gapDropTarget = multiGetGapDropTarget( gapGeometry, bounds, clientX, clientY ); if(!gapDropTarget) { hideElement(preview); return; } preview.style.left = gapDropTarget.left + 'px'; preview.style.top = gapDropTarget.top + 'px'; preview.style.width = gapDropTarget.width + 'px'; preview.style.height = gapDropTarget.height + 'px'; showElement(preview); } function updateDragImagePosition() { elMultiDragImg.style.transform = 'translate(' + (cursorX - dragScrollX - dragImageWidth / 2) + 'px, ' + (cursorY - dragScrollY - dragImageHeight / 2) + 'px)'; } function updateDragEdgeScroll(timestamp) { const clientX = cursorX - window.scrollX; const clientY = cursorY - window.scrollY; const viewportHeight = window.innerHeight; const edgeSize = Math.min(80, viewportHeight / 4); let edge = 0; if(multiDragPointerInside && clientX >= 0 && clientX < window.innerWidth && clientY >= 0 && clientY < viewportHeight) { if(clientY < edgeSize) { edge = -(1 - clientY / edgeSize); } else if(clientY > viewportHeight - edgeSize) { edge = 1 - (viewportHeight - clientY) / edgeSize; } } if(!edge) { multiDragScrollTime = null; multiDragScrollRemainder = 0; return false; } const fullscreen = document.fullscreenElement; const scroller = fullscreen ? (fullscreen.id === 'cb-enh-multi-content' ? fullscreen : null) : document.scrollingElement; const maxScroll = scroller ? Math.max(0, scroller.scrollHeight - scroller.clientHeight) : 0; const scrollTop = fullscreen ? scroller?.scrollTop : window.scrollY; if(!scroller || (edge < 0 && scrollTop <= 0) || (edge > 0 && scrollTop >= maxScroll)) { multiDragScrollTime = null; multiDragScrollRemainder = 0; return false; } const elapsed = multiDragScrollTime === null ? 1000 / 60 : Math.min(50, timestamp - multiDragScrollTime); multiDragScrollTime = timestamp; if(multiDragScrollRemainder * edge < 0) { multiDragScrollRemainder = 0; } multiDragScrollRemainder += Math.sign(edge) * edge * edge * 1100 * elapsed / 1000; const distance = Math.trunc(multiDragScrollRemainder); multiDragScrollRemainder -= distance; if(distance !== 0) { const top = Math.max(0, Math.min(maxScroll, scrollTop + distance)); if(fullscreen) { scroller.scrollTo({top: top, behavior: 'instant'}); } else { window.scrollTo({top: top, behavior: 'instant'}); } cursorX += window.scrollX - dragScrollX; cursorY += window.scrollY - dragScrollY; dragScrollX = window.scrollX; dragScrollY = window.scrollY; updateDragImagePosition(); if((fullscreen ? scroller.scrollTop : window.scrollY) === scrollTop) { multiDragScrollTime = null; multiDragScrollRemainder = 0; return false; } } return true; } function scheduleMultiPointerUpdate() { if(gMultiIsResizing) { return; } if(gMultiIsDragging) { updateDragImagePosition(); } if(multiPointerFrame !== null) { return; } multiPointerFrame = requestAnimationFrame(timestamp => { multiPointerFrame = null; multiIframePointerBounds = null; if(gMultiIsResizing) { return; } const keepScrolling = gMultiIsDragging && updateDragEdgeScroll(timestamp); updateCamHover(); if(keepScrolling) { scheduleMultiPointerUpdate(); } }); } addEventListener('mousemove', function(e) { if(multiGesturePointerId !== null) { return; } multiDragPointerInside = true; multiIframePointerBounds = null; cursorX = e.pageX; cursorY = e.pageY; dragScrollX = window.scrollX; dragScrollY = window.scrollY; if(gMultiIsDragging && e.buttons === 0) { multiOnDragEnd(true); return; } scheduleMultiPointerUpdate(); }); function multiDrop() { const oldScrollY = window.scrollY; if(gapDropTarget) { gMultiCustomGridActive = false; elDragBlock.style.gridColumn = gapDropTarget.columnStart + ' / span ' + gapDropTarget.columnSpan; elDragBlock.style.gridRow = gapDropTarget.rowStart + ' / span ' + gapDropTarget.rowSpan; multiSaveCamSizes(); window.scrollTo(0, oldScrollY); return; } const index = gMultiRooms.indexOf(elDragHoverBlock.getAttribute('data-cb-enh-username')); const oldIndex = gMultiRooms.indexOf(elDragBlock.getAttribute('data-cb-enh-username')); if(index === -1 || oldIndex === -1) { return; } if(gMultiDropReplace) { const temp = gMultiRooms[index]; gMultiRooms[index] = gMultiRooms[oldIndex]; gMultiRooms[oldIndex] = temp; const dragBlockColumn = getComputedStyle(elDragBlock).gridColumn; const dragBlockRow = getComputedStyle(elDragBlock).gridRow; elDragBlock.style.gridColumn = elDragHoverBlock.style.gridColumn; elDragBlock.style.gridRow = elDragHoverBlock.style.gridRow; elDragHoverBlock.style.gridColumn = dragBlockColumn; elDragHoverBlock.style.gridRow = dragBlockRow; multiUpdateCamOrder(); } else { setCamBlockOrder(elDragBlock, index); } window.scrollTo(0, oldScrollY); const data = multiLoadRoomsSettings(); data['rooms'] = gMultiRooms; localStorage.setItem('cb-enh-multicam', JSON.stringify(data)); multiSaveCamSizes(gMultiCustomGridActive ? 'preset' : 'custom'); multiUpdateShareURL(); } function multiOnDragEnd(cancelled) { if(!elDragBlock) { return; } if(multiPointerFrame !== null) { cancelAnimationFrame(multiPointerFrame); multiPointerFrame = null; } if(!cancelled) { gapGeometry = null; updateCamHover(); } const shouldDrop = !cancelled && (gapDropTarget || (elDragHoverBlock && elDragBlock !== elDragHoverBlock)); const dropCams = [elDragBlock]; if(!gapDropTarget && gMultiDropReplace && elDragHoverBlock) { dropCams.push(elDragHoverBlock); } const finishDrop = shouldDrop ? gMultiPrepareGridResize(gMultiCustomGridActive && !gapDropTarget ? null : dropCams) : null; gMultiIsDragging = false; multiDragScrollTime = null; multiDragScrollRemainder = 0; multiDragPointerInside = false; multiIframePointerBounds = null; releaseMultiGesture(); clearGapGeometryCache(); queryAndRun('.cb-enh-multi-video-iframe', el => { el.style.pointerEvents = 'auto'; }); const dragImg = document.getElementById('cb-enh-multi-drag-img'); hideElement(dragImg); dragImg.style.removeProperty('will-change'); clearElement(dragImg); elDragBlock.classList.remove('cb-enh-multi-cam-dragging'); document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { setCamOpacityState(el, 'cb-enh-multi-cam-ondragover', false); }); document.body.style.cursor = ''; if(shouldDrop) { try { multiDrop(); } finally { finishDrop?.(); } } hideElement(document.getElementById('cb-enh-multi-gap-drop-preview')); gapDropTarget = null; elDragBlock.classList.remove('cb-enh-multi-cam-dragging'); elDragBlock = null; elDragHoverBlock = null; } window.multiOnDragEnd = cancelled => { if(multiGesturePointerId === null) { multiOnDragEnd(cancelled); } }; function multiDragStart(elDragBlock_arg) { if(gMultiIsDragging || gMultiIsResizing || !elDragBlock_arg?.isConnected) { return false; } let elVideo = elDragBlock_arg.querySelector('video'); if(!elVideo) { elVideo = elDragBlock_arg.querySelector('.cb-enh-multi-video-iframe')?.contentDocument?.querySelector('video[data-testid="video"]'); } if(elVideo) { if((gFreezoomInstances.get(elVideo)?.getTransform().zoom ?? 1) !== 1) { return false; } } clearGridResizeSnapshots(); elDragBlock = elDragBlock_arg; elDragBlock.classList.add('cb-enh-multi-cam-dragging'); queryAndRun('.cb-enh-multi-video-iframe', el => { el.style.pointerEvents = 'none'; }); const elDragImg = document.getElementById('cb-enh-multi-drag-img'); clearElement(elDragImg); const canvas = elVideo ? captureVideoFrame(elVideo) : null; if(canvas) { elDragImg.append(canvas); } else { const elImg = elDragBlock.querySelector('img.cb-enh-multi-cam-snapshot'); if(elImg) { const elClonedImg = elImg.cloneNode(); if(elClonedImg) { if(elDragBlock.classList.contains('cb-enh-multi-offline')) { elClonedImg.classList.add('cb-enh-multi-offline'); } elDragImg.append(elClonedImg); } } } gMultiIsDragging = true; multiDragScrollTime = null; multiDragScrollRemainder = 0; multiDragPointerInside = true; dragScrollX = window.scrollX; dragScrollY = window.scrollY; elDragImg.style.willChange = 'transform'; showElement(elDragImg); dragImageWidth = elDragImg.offsetWidth; dragImageHeight = elDragImg.offsetHeight; updateDragImagePosition(); document.body.style.cursor = 'move'; startGapGeometryCache(); updateCamHover(); scheduleMultiPointerUpdate(); return true; } function cancelMultiGesture() { if(gMultiIsResizing) { finishCamResize(); } else if(gMultiIsDragging) { multiOnDragEnd(true); } else { releaseMultiGesture(); } } addEventListener('pointermove', e => { if(e.pointerId !== multiGesturePointerId) { return; } e.preventDefault(); if(e.buttons === 0) { cancelMultiGesture(); return; } multiDragPointerInside = true; if(gMultiIsResizing) { resizeCam(e); return; } cursorX = e.pageX; cursorY = e.pageY; dragScrollX = window.scrollX; dragScrollY = window.scrollY; if(multiPendingDrag) { const dx = e.clientX - multiPendingDrag.x; const dy = e.clientY - multiPendingDrag.y; if(dx * dx + dy * dy < 36) { return; } const cam = multiPendingDrag.cam; multiPendingDrag = null; if(!multiDragStart(cam)) { releaseMultiGesture(); } } else if(gMultiIsDragging) { scheduleMultiPointerUpdate(); } }); document.addEventListener('pointerup', e => { if(e.pointerId !== multiGesturePointerId) { return; } e.preventDefault(); e.stopPropagation(); if(gMultiIsResizing) { finishCamResize(e); } else if(gMultiIsDragging) { cursorX = e.pageX; cursorY = e.pageY; multiOnDragEnd(false); } else { releaseMultiGesture(); } }); for(const event of ['pointercancel', 'lostpointercapture']) { document.addEventListener(event, e => { if(e.pointerId === multiGesturePointerId) { cancelMultiGesture(); } }); } document.addEventListener('mouseleave', () => { if(multiGesturePointerId === null) { multiDragPointerInside = false; } }); window.addEventListener('blur', () => { multiDragPointerInside = false; if(multiGesturePointerId !== null) { cancelMultiGesture(); } }); addEventListener('pointerdown', function(e) { if(!e.isPrimary || e.button !== 0 || multiGesturePointerId !== null || gMultiIsResizing) { return; } if(e.target.closest('.cb-enh-vcontrols-block')) { return; } if(e.target.closest('#cb-enh-video-controls-modal')) { return; } dragIsPointerDown = e.pointerType === 'mouse'; }); addEventListener('pointerup', function(e) { if(multiGesturePointerId !== null || !e.isPrimary) { return; } dragIsPointerDown = false; if(!gMultiIsDragging) { return; } cursorX = e.pageX; cursorY = e.pageY; multiOnDragEnd(false); }); document.addEventListener('scroll', () => { multiIframePointerBounds = null; if(!gMultiIsDragging) { return; } cursorX += window.scrollX - dragScrollX; cursorY += window.scrollY - dragScrollY; dragScrollX = window.scrollX; dragScrollY = window.scrollY; scheduleMultiPointerUpdate(); }, true); document.addEventListener('dragstart', (e) => { const el = e.target.closest('.cb-enh-multi-cam'); if(!el) { return; } e.preventDefault(); e.stopImmediatePropagation(); if(gMultiIsResizing || gMultiIsDragging || multiGesturePointerId !== null) { return; } if(!dragIsPointerDown) { return; } const elDragBlock = e.target.closest('.cb-enh-multi-cam'); multiDragStart(elDragBlock); }); function multiOnMouseMove(iframe, clientX, clientY, movementX, movementY) { if(multiGesturePointerId !== null) { return; } multiDragPointerInside = true; const scrollX = window.scrollX; const scrollY = window.scrollY; if(!multiIframePointerBounds || multiIframePointerBounds.iframe !== iframe || multiIframePointerBounds.scrollX !== scrollX || multiIframePointerBounds.scrollY !== scrollY) { multiIframePointerBounds = { iframe: iframe, rect: iframe.getBoundingClientRect(), scrollX: scrollX, scrollY: scrollY }; } const rect = multiIframePointerBounds.rect; cursorX = rect.left + clientX + scrollX; cursorY = rect.top + clientY + scrollY; dragScrollX = scrollX; dragScrollY = scrollY; scheduleMultiPointerUpdate(); gActiveFreezoomInstance?.onMouseMove_2(movementX, movementY); } window.multiOnMouseMove = multiOnMouseMove; function updateCamHover() { const el = document.elementFromPoint( cursorX - window.scrollX, cursorY - window.scrollY )?.closest('.cb-enh-multi-cam'); if(el) { const fullscreen = document.fullscreenElement; const scroller = fullscreen ? (fullscreen.id === 'cb-enh-multi-content' ? fullscreen : null) : document.scrollingElement; let besideScrollbar = false; if(!getSetting('multi-align-right') && hasOverlayScrollbar(scroller)) { const right = fullscreen ? scroller.getBoundingClientRect().left + scroller.clientLeft + scroller.clientWidth : document.documentElement.clientWidth; const bounds = el.getBoundingClientRect(); besideScrollbar = bounds.left < right && bounds.right >= right - 1; } el.classList.toggle('cb-enh-multi-cam-scrollbar-edge', besideScrollbar); if(!el.classList.contains('cb-enh-multi-cam-hover')) { setCamOpacityState(el, 'cb-enh-multi-cam-hover', true); } intvClearHover = clearTimeoutEx(intvClearHover); intvClearHover = setTimeout(clearHover, gMultiHoverTime); } if(el === gMultiHoverBlock) { updateGapDropTarget(); return; } if(gMultiHoverBlock) { if(gMultiUnmuteOnHover) { if(!gMultiIframeMode) { const elVideo = gMultiHoverBlock.querySelector('video'); if(elVideo && !elVideo.muted) { elVideo.muted = true; } } else { multiSetIframeVolume(gMultiHoverBlock, true, gMultiHoverBlock._data.volume.volume); } } if(!gMultiHoverForever) { setCamOpacityState(gMultiHoverBlock, 'cb-enh-multi-cam-hover', false); } intvClearHover = clearIntervalEx(intvClearHover); gMultiHoverBlock = null; } if(!el) { if(gMultiIsDragging && elDragHoverBlock) { setCamOpacityState(elDragHoverBlock, 'cb-enh-multi-cam-ondragover', false); elDragHoverBlock = null; } updateGapDropTarget(); return; } gMultiHoverBlock = el; if(gMultiUnmuteOnHover && hasUserInteracted()) { if(!gMultiIframeMode) { const elVideo = gMultiHoverBlock.querySelector('video'); if(elVideo && elVideo.muted) { elVideo.muted = false; if(elVideo.volume == 0) { elVideo.volume = 1; } } } else { multiSetIframeVolume(gMultiHoverBlock, false, gMultiHoverBlock._data.volume.volume); } } if(!gMultiIsDragging) { return; } if(elDragHoverBlock && elDragHoverBlock !== el) { setCamOpacityState(elDragHoverBlock, 'cb-enh-multi-cam-ondragover', false); } if(elDragBlock !== el) { setCamOpacityState(el, 'cb-enh-multi-cam-ondragover', true); elDragHoverBlock = el; } else { elDragHoverBlock = null; } updateGapDropTarget(); } if(getSetting('multi-minifscreen')) { multiToggleFullscreenMini(); } multiInitSupportInfo(); localizeNextTick(); disableDynamicLinks(); window.addEventListener('focus', function() { document.querySelectorAll('video').forEach(video => { if(video.duration - video.currentTime > 40 && video.duration >= 2) { video.currentTime = video.duration - 2; } }); }); window.addEventListener('resize', function() { multiIframePointerBounds = null; if(multiGesturePointerId !== null) { cancelMultiGesture(); } else if(gMultiIsDragging) { multiOnDragEnd(true); } multiResetCamPositions(); if(gMultiCustomGridActive) { multiRefreshCustomGrid(); return; } const bodyWidth = document.body.clientWidth; document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { const columnSpan = multiGetCamColumnSpan(el); if(columnSpan !== null) { multiApplyCamColumnSpan(el, columnSpan, bodyWidth); } }); }); if(!('pictureInPictureEnabled' in document && !!document.pictureInPictureEnabled)) { addStyle('#cb-enh-pip { display:none; }') } } function multiMuteAllVideos() { if(!gMultiIframeMode) { document.querySelectorAll('.cb-enh-multi-cam video').forEach(video => { video.muted = true; }); } else { document.querySelectorAll('.cb-enh-multi-cam').forEach(elBlock => { multiSetIframeVolume(elBlock, true, 1); }); } } function multiSetIframeVolume(elBlock, muted, volume) { if(!elBlock) { return; } const elIframe = elBlock.querySelector('.cb-enh-multi-video-iframe'); if(!elIframe) { return; } sendIframeEvent(elIframe, 'set-volume', { 'volume': volume, 'muted': muted }); elBlock._data ??= {}; elBlock._data.volume = { volume: volume, muted: muted }; updateVideoUI(elBlock); } function multiInitGridSizeSelector(elNav) { const selectorDiv = document.createElement('div') selectorDiv.id = 'cb-enh-multi-size-selector'; if(gMultiInMiniFullscreen) { document.getElementById('cb-enh-multi-content').prepend(selectorDiv); } else { elNav.append(selectorDiv); } const svg0Wrap = document.createElement('div'); svg0Wrap.innerHTML = SVG_ICON_GRID_SIZE_1_2; const svg0 = svg0Wrap.firstElementChild; svg0.classList.add('cb-enh-grid-size-selector-img'); svg0.addEventListener('click', function() { multiSetGridSize(gMultiCustomGridSize); setSetting('multi-grid-size', gMultiCustomGridSize); }); selectorDiv.append(svg0); const svg1Wrap = document.createElement('div'); svg1Wrap.innerHTML = gGridIconSvgBig; const svg1 = svg1Wrap.firstElementChild; svg1.classList.add('cb-enh-grid-size-selector-img'); selectorDiv.append(svg1); svg1.addEventListener('click', function() { const columns = 2; multiSetGridSize(columns); setSetting('multi-grid-size', columns); }); const svg2Wrap = document.createElement('div'); svg2Wrap.innerHTML = gGridIconSvgMedium; const svg2 = svg2Wrap.firstElementChild; svg2.classList.add('cb-enh-grid-size-selector-img'); svg2.addEventListener('click', function() { const columns = 3; multiSetGridSize(columns); setSetting('multi-grid-size', columns); }); selectorDiv.append(svg2); const svg3Wrap = document.createElement('div'); svg3Wrap.innerHTML = gGridIconSvgSmall; const svg3 = svg3Wrap.firstElementChild; svg3.classList.add('cb-enh-grid-size-selector-img'); selectorDiv.append(svg3); svg3.addEventListener('click', function() { const columns = 4; multiSetGridSize(columns); setSetting('multi-grid-size', columns); }); const svg4Wrap = document.createElement('div'); svg4Wrap.innerHTML = gGridIconSvgSmallX; const svg4 = svg4Wrap.firstElementChild; svg4.classList.add('cb-enh-grid-size-selector-img'); selectorDiv.append(svg4); svg4.addEventListener('click', function() { const columns = 5; multiSetGridSize(columns); setSetting('multi-grid-size', columns); }); } function multiInitSupportInfo() { const userType = getUserType(); const msg = getSupportMessage(userType); if(msg === null) { queryHideElement('#'+gtrrft('mspr')); return; } document.addEventListener('click', (e) => { const el = e.target.closest('#'+gtrrft('mspr')); if(!el) { return; } loutrreg(gCurrentBroadcaster); }); document.getElementById(gtrrft('mspr')).insertAdjacentHTML('beforeend', msg); } function isFollowedCamsLink(el) { const url = new URL(el.getAttribute('href'), window.location.origin); return url.pathname.startsWith('/followed-cams'); } function setRecordWarningModalShown(show) { setPageOverlayShown(show); document.getElementById('cb-enh-record-warn-modal').style.display = show ? 'flex' : 'none'; } function disableDynamicLinksHandler(e) { const elLink1 = e.target.closest('a.FollowedDropdown__show-all'); if(elLink1) { e.stopImmediatePropagation(); if(gRecording) { e.preventDefault(); setRecordWarningModalShown(true); } return; } const elLink2 = e.target.closest('a'); if(elLink2) { if(isFollowedCamsLink(elLink2)) { return; } if(elLink2.classList.contains('tabLink')) { return; } if(!elLink2.href) { return; } if(elLink2.href === '#') { return; } if(elLink2.target == '_blank') { return; } if(elLink2.getAttribute('href').startsWith('/photo_videos/')) { return; } e.stopImmediatePropagation(); if(gRecording) { e.preventDefault(); setRecordWarningModalShown(true); } return; } } let gDynamicLinksDisabled = false; function disableDynamicLinks() { if(gDynamicLinksDisabled) { return; } document.addEventListener('click', disableDynamicLinksHandler, true); gDynamicLinksDisabled = true; } function enableDynamicLinks() { if(!gDynamicLinksDisabled) { return; } document.removeEventListener('click', disableDynamicLinksHandler, true); gDynamicLinksDisabled = false; } const acre5 = 'ugVacTnBjqAPShC2UAF1'; function multiValidateCamSizes(sizes, gridColumns) { const validated = Object.create(null); if(!Number.isSafeInteger(gridColumns) || gridColumns <= 0 || !sizes || typeof sizes !== 'object' || Array.isArray(sizes)) { return validated; } for(const [username, span] of Object.entries(sizes)) { if(!/^[a-zA-Z0-9_]+$/.test(username) || !Number.isSafeInteger(span) || span <= 0 || span > gridColumns) { continue; } validated[username] = span; } return validated; } function multiLoadRoomsSettings() { let data = localStorage.getItem('cb-enh-multicam'); if(data) { try { data = JSON.parse(data); } catch(e) { data = null; } } if(!data || typeof data !== 'object' || !Array.isArray(data.rooms)) { data = { 'rooms': [] }; } data.sizes = multiValidateCamSizes(data.sizes, data.gridColumns); if(!Number.isSafeInteger(data.gridColumns) || data.gridColumns <= 0) { data.gridColumns = gMultiGridColumnCount; } return data; } function multiGetCamColumnSpan(el) { const match = el.style.gridColumn.match(/\bspan\s+(\d+)/); return match ? Number(match[1]) : null; } function multiApplyCamColumnSpan(el, span, bodyWidth = document.body.clientWidth) { span = Math.min(gMultiGridColumnCount, Math.max(gMultiMinColumnSpan, Math.round(span))); const rowSpan = Math.max(1, Math.round(bodyWidth * span / gMultiGridColumnCount * 9 / 16)); el.style.gridColumn = 'span ' + span; el.style.gridRow = 'span ' + rowSpan; } function multiRestoreCamSize(el, data, bodyWidth) { const span = data.sizes[el.getAttribute('data-cb-enh-username')]; if(span === undefined) { return; } multiApplyCamColumnSpan(el, span / data.gridColumns * gMultiGridColumnCount, bodyWidth); } function multiSaveCamSizes(layoutMode = 'custom', resetSizes = false) { const data = multiLoadRoomsSettings(); if(resetSizes) { data.sizes = Object.create(null); } else if(data.gridColumns !== gMultiGridColumnCount) { for(const [username, span] of Object.entries(data.sizes)) { data.sizes[username] = Math.min(gMultiGridColumnCount, Math.max(gMultiMinColumnSpan, Math.round(span / data.gridColumns * gMultiGridColumnCount))); } } data.gridColumns = gMultiGridColumnCount; document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { const span = multiGetCamColumnSpan(el); if(span === null) { return; } data.sizes[el.getAttribute('data-cb-enh-username')] = Math.min(gMultiGridColumnCount, Math.max(gMultiMinColumnSpan, span)); }); data.layoutMode = layoutMode; const json = JSON.stringify(data); if(localStorage.getItem('cb-enh-multicam') !== json) { localStorage.setItem('cb-enh-multicam', json); } } function multiUpdateRooms() { const data = multiLoadRoomsSettings(); data['rooms'].forEach(function(room) { if(gMultiRooms.indexOf(room) !== -1) { return; } multiAddRoom(room); }); } function multiUpdateRoomStatuses() { gMultiRooms.forEach(function(username) { const elRoomBlock = document.querySelector('.cb-enh-multi-cam[data-cb-enh-username="' + username + '"]'); if(gMultiRoomsData[username]['is_loading']) { return; } if(elRoomBlock) { if(elRoomBlock.querySelector('.cb-enh-multi-video-iframe')) { return; } const elVideo = elRoomBlock ? elRoomBlock.querySelector('video') : null; const streamPlaying = elVideo && isVideoPlaying(elVideo); if(streamPlaying) { return; } } multiFetch(username, false); }); } function multiSetErrored(username, block, status) { if(!block) { return; } block.querySelector('.cb-enh-multi-status').innerText = status; block.classList.add('cb-enh-multi-offline'); block.querySelector('video')?.remove(); if(multiDoAutoHideOffline()) { hideElement(block); multiDestroyVideo(username); } else { showElement(block); } multiRefreshCustomGrid(); } function multiSetOk(block) { block.querySelector('.cb-enh-multi-status').innerText = ''; block.classList.remove('cb-enh-multi-offline'); showElement(block); multiRefreshCustomGrid(); } function setSliderPosition(el, pos) { pos = pos*100/2; const elDot = el.querySelector('.cb-enh-vcontrols-slider-dot'); const elBase = el.querySelector('.cb-enh-vcontrols-slider-base'); elDot.style.left = pos + 'px'; elBase.style.width = pos + 'px'; } function updateVideoUI(elBlock) { if(!elBlock) { return; } let muted = true; let volume = 0; if(gIsInMultiView) { if(!gMultiIframeMode) { const elVideo = elBlock.querySelector('video'); muted = elVideo.muted; volume = elVideo.volume; } else { muted = elBlock._data.volume.muted; volume = elBlock._data.volume.volume; } } else { const elVideo = elBlock.classList.contains('cb-enh-rewinded') ? getVideoHolderRewindVideo(elBlock) : getVideoHolderVideo(elBlock); muted = elVideo.muted; volume = elVideo.volume; } let icon = 'high'; if(muted) { icon = 'mute'; } else if(volume < 0.34) { icon = 'low'; } else if(volume < 0.67) { icon = 'medium'; } const elBtnImg = elBlock.querySelector('.cb-enh-vcontrols-btn-vol img'); elBtnImg.src = 'https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/volume-' + icon + '.svg'; setSliderPosition(elBlock.querySelector('.cb-enh-vcontrols-btn.slider'), muted ? 0 : volume); elBtnImg.parentElement.querySelector('.video-controls-tooltip p').innerText = muted ? getLocale('unmute') : getLocale('mute'); } function multiInitVideo(block, username, hls_source) { multiDestroyVideo(username); if(gMultiIframeMode) { let url = `/fullvideo/?b=${username}&signup_notice=0&mobileRedirect=never&i`; if(gMultiRoomsData[username]['opt_out']) { url = `/fullvideo/?b=multicam&n=${username}&signup_notice=0&mobileRedirect=never&i`; } const videoHTML = `<iframe class="cb-enh-video cb-enh-multi-video-iframe" src="${url}"></iframe>`; const template = document.createElement('template'); template.innerHTML = videoHTML.trim(); const elVideo = template.content.firstElementChild; elVideo.addEventListener('load', () => { elVideo.contentWindow.document.addEventListener('pointerdown', e => { if(e.isPrimary && e.pointerType === 'touch' && !gMultiIsDragging && !gMultiIsResizing) { window.multiOnMouseMove(elVideo, e.clientX, e.clientY, 0, 0); } }); elVideo.contentWindow.document.addEventListener('pointerup', e => { if(!gMultiIsDragging) { return; } window.multiOnDragEnd(false); }); elVideo.contentWindow.document.addEventListener('mouseup', e => { gActiveFreezoomInstance?.eventHandlers.onMouseUp(); }); elVideo.contentWindow.document.addEventListener('mousemove', e => { window.multiOnMouseMove(elVideo, e.clientX, e.clientY, e.movementX, e.movementY); }); }); block.querySelector('.cb-enh-multi-cam-video-wrap')?.append(elVideo); return; } let videoHTML = '<video autoplay muted class="vjs-tech cb-enh-video" id="vjs_video_3_html5_api" tabindex="-1" role="application" poster="https://jpeg.live.mmcdn.com/stream?room={{username}}&f={{random}}"></video>'; videoHTML = videoHTML.replaceAll('{{username}}', username); videoHTML = videoHTML.replaceAll('{{random}}', Math.random()); const template = document.createElement('template'); template.innerHTML = videoHTML.trim(); const elVideo = template.content.firstElementChild; const hls = createHLS(); hls.config.preserveManualLevelOnError = getSetting('error-quality') === false; gMultiRoomsData[username]['hls'] = hls; hls.on(Hls.Events.MANIFEST_PARSED, function() { multiUpdateVideoQuality(this); this.currentLevel = this.autoLevelCapping; this.startLoad(); let levelIndex = multiUpdateVideoQuality(this, BIG_PREVIEW_MAX_RESOLUTION, false); if(levelIndex === null && this.levels && this.levels.length !== 0) { levelIndex = this.levels.length - 1; } let levelUrl = null; if(levelIndex !== null) { levelUrl = cutLevelUrl(this.levels[levelIndex].url[0]); } bigPreviewSetCachedHLSURL(username, this.url, levelUrl); }); hls.loadSource(hls_source); hls.attachMedia(elVideo); elVideo.addEventListener('ended', () => { multiFetch(username, false); }); block.querySelector('.cb-enh-multi-cam-video-wrap')?.append(elVideo); } function multiDestroyVideo(username) { if(!gMultiRoomsData[username]) { return; } if('hls' in gMultiRoomsData[username] && gMultiRoomsData[username]['hls']) { gMultiRoomsData[username]['hls'].stopLoad(); gMultiRoomsData[username]['hls'].detachMedia(); gMultiRoomsData[username]['hls'].destroy(); } const elRoomBlock = document.querySelector('.cb-enh-multi-cam[data-cb-enh-username="' + username + '"]'); if(!elRoomBlock) { return; } setFreezoomEnabled(elRoomBlock, false); const elIframe = elRoomBlock.querySelector('.cb-enh-multi-video-iframe'); if(elIframe) { elIframe.remove(); destroyVideoDurationCounter(username); } const elVideo = elRoomBlock.querySelector('video'); if(!elVideo) { return; } elVideo.pause(); if(elVideo.src && elVideo.src.startsWith('blob:')) { URL.revokeObjectURL(elVideo.src); } elVideo.removeAttribute('src'); elVideo.removeAttribute('poster'); elVideo.load(); elVideo.remove(); destroyVideoDurationCounter(username); } function multiGetMaxHeight() { const minMaxHeight = 480; const maxMaxHeight = 2160; const defaultMaxHeight = 1080; let maxHeight = parseInt(getSetting('multi-max-quality'), 10); if(!Number.isInteger(maxHeight)) { maxHeight = defaultMaxHeight; } return clampInt(maxHeight, minMaxHeight, maxMaxHeight); } function multiUpdateVideoQuality(hls, maxHeight = null, doSetLevel = true) { if(maxHeight === null) { maxHeight = multiGetMaxHeight(); } let capped = false; let setLevel = null; hls.levels.every(function(level, i) { if(level.height > maxHeight) { let levelIndex = i - 1; if(levelIndex < 0) { levelIndex = 0; } if(doSetLevel) { hls.autoLevelCapping = levelIndex; } setLevel = levelIndex; capped = true; return false; } return true; }); if(!capped && doSetLevel) { hls.autoLevelCapping = -1; } return setLevel; } function multiUpdateAllVideosQuality() { document.querySelectorAll('.cb-enh-multi-video-iframe').forEach(elIframe => { sendIframeEvent(elIframe, 'update-quality'); }); document.querySelectorAll('.cb-enh-multi-cam video').forEach(el => { const username = el.closest('.cb-enh-multi-cam').getAttribute('data-cb-enh-username'); const hls = gMultiRoomsData[username]['hls']; if(hls) { multiUpdateVideoQuality(hls); } }); } function multiFetch(username, an) { const block = document.querySelector('.cb-enh-multi-cam[data-cb-enh-username="' + username + '"]'); const roomData = gMultiRoomsData[username]; if(!block || !roomData) { return; } function onRequestError() { if(gMultiRoomsData[username] !== roomData) { return; } roomData['is_loading'] = false; const elVideo = block.querySelector('video'); const isIframeModeAndLoaded = block.querySelector('.cb-enh-multi-video-iframe') !== null; const streamPlaying = (elVideo && isVideoPlaying(elVideo)) || isIframeModeAndLoaded; if(!streamPlaying) { multiSetErrored(username, block, getLocale('error')); } } function innerMultiFetch() { (an ? xmlhttpRequest : xmlhttpRequest_tryNative)({ method: 'GET', url: '/api/chatvideocontext/' + username + '/', timeout: 60*1*1000, anonymous: an, onload: function(resp) { if(gMultiRoomsData[username] !== roomData) { return; } roomData['is_loading'] = false; const elVideo = block ? block.querySelector('video') : null; const isIframeModeAndLoaded = block.querySelector('.cb-enh-multi-video-iframe') !== null; const streamPlaying = (elVideo && isVideoPlaying(elVideo)) || isIframeModeAndLoaded; let data; try { data = JSON.parse(resp.responseText); } catch(e) { if(!streamPlaying) { multiSetErrored(username, block, getLocale('error')); } return; } if(data.status === 403 && data.code === 'access-denied') { if(an) { if(!streamPlaying) { multiSetErrored(username, block, getLocale('error')); } return; } multiFetch(username, true); return; } let optOut = false; if('opt_out' in data) { optOut = data['opt_out']; } gMultiRoomsData[username]['opt_out'] = optOut; const autoHideOffline = multiDoAutoHideOffline(); const autoHidePriv = getSetting('multi-auto-hide-priv'); if('start_timestamp' in data) { const secondsOnline = Date.now() / 1000 - Number(data['start_timestamp']); initVideoDurationCounter(username, block, null, secondsOnline); } if(!('room_status' in data)) { if('code' in data && data['code'] === 'password-required') { multiSetErrored(username, block, getLocale('passworded')); if(autoHidePriv) { hideElement(block); multiDestroyVideo(username); multiRefreshCustomGrid(); } return; } multiSetErrored(username, block, getLocale('error')); return; } const status = data['room_status']; const hasValidSource = 'hls_source' in data && data['hls_source'] !== ''; const doHideBlock = (autoHideOffline && status === 'offline') || (autoHidePriv && status !== 'public' && status !== 'offline'); if(!doHideBlock && gMultiIframeMode && !isIframeModeAndLoaded) { multiInitVideo(block, username, null); } if(!gMultiIframeMode && status === 'public') { multiSetOk(block); if(hasValidSource) { const elVideo = block.querySelector('.cb-enh-video'); if(!elVideo) { multiInitVideo(block, username, cutPlaylistUrl(data['hls_source'])); } else if(!gMultiIframeMode) { const trans = getCBTrans(data['hls_source']); if(trans && getCBTrans(gMultiRoomsData[username]['hls'].url) !== trans) { gMultiRoomsData[username]['hls'].stopLoad(); gMultiRoomsData[username]['hls'].detachMedia(); gMultiRoomsData[username]['hls'].loadSource(cutPlaylistUrl(data['hls_source'])); gMultiRoomsData[username]['hls'].attachMedia(elVideo); } } } else { if(!streamPlaying) { multiSetErrored(username, block, getLocale('multi_no_video_hls') ); } } } multiUpdateRoomStatus(block, status); if(gMultiRoomsData[username] !== roomData) { return; } block.querySelector('.cb-enh-multi-follow:not(.cb-enh-multi-follow-update)').classList.toggle('cb-enh-multi-follow-followed', data['following']); block.querySelector('.cb-enh-multi-subject').textContent = filterSubject(username, data['room_title']); }, onerror: onRequestError, ontimeout: onRequestError, onabort: onRequestError }); } roomData['is_loading'] = true; isRoomOnline(username, (isOnline) => { if(gMultiRoomsData[username] !== roomData) { return; } if(!isOnline) { const elVideo = block ? block.querySelector('video') : null; const streamPlaying = elVideo && isVideoPlaying(elVideo); if(!streamPlaying) { isRoomOnline_2(username, (isOnline_2) => { if(gMultiRoomsData[username] !== roomData) { return; } if(!isOnline_2) { roomData['is_loading'] = false; multiUpdateRoomStatus(block, 'offline'); return; } if(gMultiIframeMode) { innerMultiFetch(); } else { roomData['is_loading'] = false; } }, onRequestError); return; } roomData['is_loading'] = false; return; } innerMultiFetch(); }, onRequestError); } function filterSubject(username, subject) { if(subject.toLowerCase().trim() === username + "'s room") { return ''; } return subject; } function multiAddRoom(username) { if(!username) { return; } if(gMultiRooms.includes(username)) { return; } gMultiRoomsData[username] = { 'is_loading': true, 'status': null, 'hls': null }; gMultiRooms.push(username); let blockHTML = multiViewCamHTML; blockHTML = blockHTML.replaceAll('{{username}}', username); blockHTML = blockHTML.replaceAll('{{random}}', Math.random()); blockHTML = blockHTML.replaceAll('{{SVG_CLOSE_ICON}}', SVG_CLOSE_ICON); const template = document.createElement('template'); template.innerHTML = blockHTML.trim(); const block = template.content.firstElementChild; block._data ??= {}; block._data.volume = { volume: 1, muted: true }; const elCams = document.getElementById('cb-enh-multi-cams'); block.style.order = gMultiRooms.length - 1; elCams.appendChild(block); const minGridSize = 2; const maxGridSize = 10; let gridSize = getSetting('multi-grid-size'); gridSize = parseInt(gridSize); if(Number.isInteger(gridSize)) { if(gridSize !== gMultiCustomGridSize && gridSize < minGridSize) { gridSize = minGridSize; } else if(gridSize > maxGridSize) { gridSize = maxGridSize } } else { gridSize = 3; } const width = Math.floor(document.body.clientWidth / (gridSize === gMultiCustomGridSize ? 3 : gridSize)); const columnEnd = Math.max(gMultiMinColumnSpan, Math.round((width / document.body.clientWidth) * gMultiGridColumnCount)); block.style.gridColumn = 'span ' + columnEnd.toString(); const widthReal = (columnEnd / gMultiGridColumnCount) * document.body.clientWidth; const height = widthReal * 9/16 const rowCount = Math.round(height); block.style.gridRow = 'span ' + rowCount.toString(); if(gridSize === gMultiCustomGridSize && gMultiRooms.length === 1) { block.style.gridColumn = 'span ' + columnEnd * 2; block.style.gridRow = 'span ' + rowCount * 2; } multiRestoreCamSize(block, multiLoadRoomsSettings()); multiRefreshCustomGrid(); multiFetch(username, false); multiUpdateRoomSnapshot(block, username); insertRoomAv(block.querySelector('.cb-enh-multi-cam-img-wrap'), username); multiUpdateShareURL(); localizeNextTick(); } function multiUpdateRoomStatus(elBlock, status) { if(!elBlock) { return; } const username = elBlock.getAttribute('data-cb-enh-username'); if(!gMultiRoomsData[username]) { return; } gMultiRoomsData[username]['status'] = status; if(status === 'offline' && getSetting('multi-auto-remove')) { multiRemoveRoom(username); return; } const autoHideOffline = multiDoAutoHideOffline(); const autoHidePriv = getSetting('multi-auto-hide-priv'); const doHideBlock = (autoHideOffline && status === 'offline') || (autoHidePriv && status !== 'public' && status !== 'offline'); if(status === 'offline' || (!gMultiIframeMode && status !== 'public') || (gMultiIframeMode && (autoHidePriv && status !== 'public' && status !== 'offline'))) { const statusText = getLocale('status_' + status) || status; multiSetErrored(username, elBlock, statusText); } else { multiSetOk(elBlock); const elVideo = elBlock.querySelector('.cb-enh-video'); if(!elVideo) { multiInitVideo(elBlock, username, cutPlaylistUrl(data['hls_source'])); } else if(!gMultiIframeMode) { const trans = getCBTrans(data['hls_source']); if(trans && getCBTrans(gMultiRoomsData[username]['hls'].url) !== trans) { gMultiRoomsData[username]['hls'].stopLoad(); gMultiRoomsData[username]['hls'].detachMedia(); gMultiRoomsData[username]['hls'].loadSource(cutPlaylistUrl(data['hls_source'])); gMultiRoomsData[username]['hls'].attachMedia(elVideo); } } } if(doHideBlock) { hideElement(elBlock); multiDestroyVideo(username); } else { showElement(elBlock); } multiRefreshCustomGrid(); } function multiUpdateRoomSnapshot(elBlock, username) { fetchRoomSnapshot(username, function(resp) { if(!elBlock) { return; } let elImg = elBlock.querySelector('.cb-enh-multi-cam-snapshot') if(!elImg) { elImg = document.createElement('img'); elImg.classList.add('cb-enh-multi-cam-snapshot'); elBlock.querySelector('.cb-enh-multi-cam-img-wrap')?.appendChild(elImg); } elImg.src = 'data:image/jpg;base64,' + btoa(String.fromCharCode.apply(null, new Uint8Array(resp))); }); } function multiRemoveRoom(username) { const pos = gMultiRooms.indexOf(username); multiDestroyVideo(username); if(pos !== -1) { gMultiRooms.splice(pos, 1); delete gMultiRoomsData[username]; } const data = multiLoadRoomsSettings(); data.rooms = gMultiRooms; delete data.sizes[username]; const elRoomBlock = document.querySelector('.cb-enh-multi-cam[data-cb-enh-username="' + username + '"]'); if(elRoomBlock) { elRoomBlock.remove(); } localStorage.setItem('cb-enh-multicam', JSON.stringify(data)); multiUpdateCamOrder(); multiUpdateShareURL(); } function multiAddRoomToList(username) { if(!username) { return; } const data = multiLoadRoomsSettings(); if(data['rooms'].indexOf(username) !== -1) { return; } data['rooms'].push(username); localStorage.setItem('cb-enh-multicam', JSON.stringify(data)); multiUpdateShareURL(); } function multiClearRoomList() { const data = multiLoadRoomsSettings(); data['rooms'] = []; localStorage.setItem('cb-enh-multicam', JSON.stringify(data)); } function multiFindGapDropTarget(occupied, columnWidth, rowCount, pointerColumn, pointerRow) { let best = null; let bestGapArea = 0; const tops = new Int32Array(gMultiGridColumnCount); const bottoms = new Int32Array(gMultiGridColumnCount).fill(rowCount); const blockedColumns = new Uint8Array(gMultiGridColumnCount); for(const rect of occupied) { const left = Math.max(0, rect.left); const right = Math.min(gMultiGridColumnCount, rect.right); for(let column = left; column < right; column++) { if(rect.bottom <= pointerRow) { tops[column] = Math.max(tops[column], rect.bottom); } else if(rect.top > pointerRow) { bottoms[column] = Math.min(bottoms[column], rect.top); } else { blockedColumns[column] = 1; } } } for(let left = 0; left <= pointerColumn; left++) { let top = 0; let bottom = rowCount; let blocked = false; for(let column = left; column < pointerColumn; column++) { if(blockedColumns[column]) { blocked = true; break; } top = Math.max(top, tops[column]); bottom = Math.min(bottom, bottoms[column]); } if(blocked) { continue; } for(let right = pointerColumn + 1; right <= gMultiGridColumnCount; right++) { if(blockedColumns[right - 1]) { break; } top = Math.max(top, tops[right - 1]); bottom = Math.min(bottom, bottoms[right - 1]); const columnSpan = Math.min(right - left, Math.floor((bottom - top) * 16 / 9 / columnWidth)); const rowSpan = Math.round(columnSpan * columnWidth * 9 / 16); if(columnSpan < gMultiMinColumnSpan || rowSpan < 1 || rowSpan > bottom - top) { continue; } const gapArea = (right - left) * (bottom - top); if(best && top + 1 > best.rowStart) { continue; } if(best && top + 1 === best.rowStart && (columnSpan < best.columnSpan || (columnSpan === best.columnSpan && gapArea <= bestGapArea))) { continue; } best = { columnStart: left + 1, columnSpan: columnSpan, rowStart: top + 1, rowSpan: rowSpan }; bestGapArea = gapArea; } } return best; } function multiMeasureGapGeometry(grid, bounds) { const columnWidth = bounds.width / gMultiGridColumnCount; const rtl = getComputedStyle(grid).direction === 'rtl'; const occupied = []; for(const block of grid.children) { if(!block.classList.contains('cb-enh-multi-cam')) { continue; } const rect = block.getBoundingClientRect(); if(rect.width <= 0 || rect.height <= 0) { continue; } occupied.push({ left: Math.round((rtl ? bounds.right - rect.right : rect.left - bounds.left) / columnWidth), right: Math.round((rtl ? bounds.right - rect.left : rect.right - bounds.left) / columnWidth), top: Math.round(rect.top - bounds.top), bottom: Math.round(rect.bottom - bounds.top) }); } return {width: bounds.width, height: bounds.height, columnWidth: columnWidth, rtl: rtl, occupied: occupied}; } function multiGetGapDropTarget(geometry, bounds, clientX, clientY) { if(bounds.width <= 0 || clientX < bounds.left || clientX >= bounds.right || clientY < bounds.top || clientY >= bounds.bottom) { return null; } const {columnWidth, rtl, occupied} = geometry; const pointerColumn = Math.min(gMultiGridColumnCount - 1, Math.floor((rtl ? bounds.right - clientX : clientX - bounds.left) / columnWidth)); const pointerRow = clientY - bounds.top; const target = multiFindGapDropTarget(occupied, columnWidth, Math.floor(bounds.height), pointerColumn, pointerRow); if(!target) { return null; } target.width = target.columnSpan * columnWidth; target.height = target.rowSpan; target.left = rtl ? bounds.right - (target.columnStart - 1) * columnWidth - target.width : bounds.left + (target.columnStart - 1) * columnWidth; target.top = bounds.top + target.rowStart - 1; return target; } function multiResetCamPositions() { document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { if(el.style.gridColumnEnd.startsWith('span ')) { el.style.gridColumn = el.style.gridColumnEnd; } if(el.style.gridRowEnd.startsWith('span ')) { el.style.gridRow = el.style.gridRowEnd; } }); } function multiRefreshCustomGrid(saveSizes = true) { if(!gMultiCustomGridActive) { return; } const cams = [...document.querySelectorAll('.cb-enh-multi-cam')] .sort((a, b) => Number(a.style.order) - Number(b.style.order)); const featuredCam = cams.find(el => el.style.display !== 'none'); const columnSpan = Math.round(gMultiGridColumnCount / 3); const rowSpan = Math.max(1, Math.round(document.body.clientWidth * columnSpan / gMultiGridColumnCount * 9 / 16)); let sizesChanged = false; cams.forEach(el => { const scale = el === featuredCam ? 2 : 1; const gridColumn = 'span ' + (el === featuredCam ? gMultiGridColumnCount - columnSpan : columnSpan); const gridRow = 'span ' + rowSpan * scale; if(el.style.gridColumn !== gridColumn) { el.style.gridColumn = gridColumn; sizesChanged = true; } if(el.style.gridRow !== gridRow) { el.style.gridRow = gridRow; } }); if(saveSizes && sizesChanged) { multiSaveCamSizes('preset'); } } function multiSetGridSize(columns) { const finishResize = gMultiPrepareGridResize?.(); try { gMultiCustomGridActive = columns === gMultiCustomGridSize; if(gMultiCustomGridActive) { multiRefreshCustomGrid(false); multiSaveCamSizes('preset', true); return; } const width = Math.floor(document.body.clientWidth / columns); const columnEnd = Math.max(gMultiMinColumnSpan, Math.round((width / document.body.clientWidth) * gMultiGridColumnCount)); document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { el.style.gridColumn = `span ${columnEnd}`; }); const widthReal = (columnEnd / gMultiGridColumnCount) * document.body.clientWidth; const height = widthReal * 9 / 16; const rowCount = Math.round(height); document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { el.style.gridRow = `span ${rowCount}`; }); multiSaveCamSizes('preset', true); } finally { finishResize?.(); } } function multiRemoveOfflineRooms() { document.querySelectorAll('.cb-enh-multi-offline').forEach(el => { const username = el.dataset.cbEnhUsername; if(gMultiRoomsData[username]['status'] !== 'offline') { return; } multiRemoveRoom(username); }); } function multiHidePrivateRoomsIfNeeded() { const autoHideOffline = multiDoAutoHideOffline(); const autoHidePriv = getSetting('multi-auto-hide-priv'); document.querySelectorAll('.cb-enh-multi-cam').forEach(el => { const username = el.dataset.cbEnhUsername; const status = gMultiRoomsData?.[username]?.status; const hide = (autoHideOffline && status === 'offline') || (autoHidePriv && status !== 'public' && status !== 'offline'); if(hide) { hideElement(el); multiDestroyVideo(username); } else { showElement(el); } }); multiRefreshCustomGrid(); } function multiSetAlignRight(enable) { document.getElementById('cb-enh-multi-content').classList.toggle('cb-enh-multi-rtl', enable); } function multiRemoveAllRooms() { document.querySelectorAll('.cb-enh-multi-cam').forEach(function(el) { const username = el.dataset.cbEnhUsername; multiRemoveRoom(username); }); } function multiUpdateShareURL() { const elShare = document.getElementById('cb-enh-multi-share'); if(!elShare) { return; } let url = 'https://' + window.location.hostname + '/multicam/'; if(gMultiRooms.length !== 0) { url += '?rooms=' + gMultiRooms.join(','); } document.getElementById('cb-enh-multi-share').value = url; } function multiEnterFullscreen() { if(document.fullscreenElement) { return; } const elContent = document.getElementById('cb-enh-multi-content'); elContent.requestFullscreen(); elContent.classList.add('cb-enh-multi-fullscreen'); elContent.classList.add('cb-enh-multi-fullscreen-full'); document.getElementById('cb-enh-multi-btn-enter-fullscreen').value = getLocale('multi_btn_exit_fscreen'); setSetting('multi-minifscreen', true); } function multiToggleFullscreenMini() { if(document.fullscreenElement) { document.exitFullscreen(); } const elBtn = document.getElementById('cb-enh-multi-btn-enter-fullscreen-mini'); const elContent = document.getElementById('cb-enh-multi-content'); if(!gMultiInMiniFullscreen) { elContent.classList.add('cb-enh-multi-fullscreen'); queryHideElement('#header'); queryHideElement('#footer-holder'); queryHideElement('#desktop-spa-header'); elBtn.value = getLocale('multi_btn_exit_fscreen'); } else { elContent.classList.remove('cb-enh-multi-fullscreen'); queryShowElement('#header'); queryShowElement('#footer-holder'); queryShowElement('#desktop-spa-header'); elBtn.value = getLocale('multi_btn_enter_fscreen_mini'); } gMultiInMiniFullscreen = !gMultiInMiniFullscreen; multiToggleSizeSelectorPosition(); setSetting('multi-minifscreen', gMultiInMiniFullscreen); window.scrollTo(0, 0); } function multiToggleSizeSelectorPosition() { const elSizeSelector = document.getElementById('cb-enh-multi-size-selector'); if(!elSizeSelector) { return; } const inNav = !document.fullscreenElement && !gMultiInMiniFullscreen; if(inNav) { const elNav = document.querySelector(navSelector); if(elNav) { elNav.append(elSizeSelector); } } else { document.getElementById('cb-enh-multi-content').prepend(elSizeSelector); } } function onFullscreenChange() { if(isMultiViewPage()) { setVideoControlsVisible(false); if(document.fullscreenElement && document.fullscreenElement.classList.contains('cb-enh-multi-cam-wrap2')) { document.querySelector('#cb-enh-multi-content')?.classList.toggle('cb-enh-multi-fullscreen-cam', true); const el = document.fullscreenElement.querySelector('.cb-enh-vcontrols-btn-fs img'); if(el) { el.src = 'https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/exit-fullscreen.svg'; el.parentElement.querySelector('.video-controls-tooltip p').innerText = getLocale('fsx'); } } else { document.querySelector('#cb-enh-multi-content')?.classList.toggle('cb-enh-multi-fullscreen-cam', false); queryAndRun('.cb-enh-vcontrols-btn-fs img[src$="/exit-fullscreen.svg"]', el => { el.src = 'https://web.static.mmcdn.com/tsdefaultassets/theaterVideoControlsIcons/fullscreen.svg'; el.parentElement.querySelector('.video-controls-tooltip p').innerText = getLocale('fs'); }); } multiToggleSizeSelectorPosition(); } if(isMultiViewPage() && !document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement) { if(!gMultiInMiniFullscreen) { document.querySelector('#cb-enh-multi-content')?.classList.remove('cb-enh-multi-fullscreen'); } document.querySelector('#cb-enh-multi-content')?.classList.remove('cb-enh-multi-fullscreen-full'); document.querySelector('#cb-enh-multi-btn-enter-fullscreen').value = getLocale('multi_btn_enter_fscreen'); return; } if(document.fullscreenElement) { queryAndRun('#cb-enh-theater-video-controls-btn', el => { el.style.display = 'inline-flex'; }); } else { queryHideElement('#cb-enh-theater-video-controls-btn'); } } function initGenderNavigation() { const observerOptions = { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style', 'data-testid'], characterData: true }; const observer = new MutationObserver(mutations => { const navigationChanged = mutations.some(mutation => { const el = mutation.target.nodeType === Node.ELEMENT_NODE ? mutation.target : mutation.target.parentElement; if(el?.closest('a.cb-enh-gender-nav-btn')) { return false; } if(el?.closest('.GenderNav')) { return true; } return Array.from(mutation.addedNodes).some(node => { return node.nodeType === Node.ELEMENT_NODE && (node.matches('.GenderNav') || node.querySelector('.GenderNav')); }); }); if(navigationChanged) { updateNavigation(); } }); function updateNavigation() { observer.disconnect(); try { convertNavButtonsToLinks(); } finally { observer.observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('.GenderNav').forEach(el => { observer.observe(el, observerOptions); }); } } updateNavigation(); window.addEventListener('popstate', updateNavigation); window.navigation?.addEventListener('currententrychange', updateNavigation); } function convertNavButtonsToLinks() { const isFollowedOfflinePage = /^\/followed-cams\/offline(?:\/|$)/.test(window.location.pathname); queryAndRun('.GenderNav__scrollContainer .GenderNav__button', el => { const testId = el.getAttribute('data-testid'); if(!testId?.startsWith('gender-nav-')) { return; } const hideOnOffline = isFollowedOfflinePage && testId !== 'gender-nav-'; const elExistingLink = el.nodeName === 'BUTTON' ? el.closest('.GenderNav__scrollContainer').querySelector('a.cb-enh-gender-nav-btn[data-testid="' + testId + '"]') : null; if(el.matches('a.cb-enh-gender-nav-btn')) { el.style.display = hideOnOffline ? 'none' : ''; } if(hideOnOffline || (isElementHidden(el) && !elExistingLink)) { return; } const isIndex = testId.endsWith('-'); let prefix = null; let url = '/'; const prefixedPages = [ '/followed-cams/', '/spy-on-cams/', '/tags/', '/discover/' ]; for(const v of prefixedPages) { if(window.location.pathname.startsWith(v)) { prefix = v; break; } } if(prefix) { url = prefix; } if(!isIndex) { const genderShort = testId.at(-1); if(!genderShort) { return; } const genderLong = getLongGenderName(genderShort); if(!genderLong) { return; } url += genderLong; } if(!prefix && !isIndex) { url += '-cams'; } if(!isIndex) { url += '/'; } const elLink = el.nodeName === 'A' ? el : elExistingLink || document.createElement('a'); elLink.href = url; if(el.nodeName === 'BUTTON') { elLink.className = el.className; elLink.setAttribute('type', 'button'); elLink.setAttribute('data-testid', testId); elLink.textContent = el.textContent; elLink.classList.add('cb-enh-gender-nav-btn'); if(el.style.display !== 'none') { hideElement(el); } if(el.nextElementSibling !== elLink) { el.after(elLink); } } }); queryAndRun('a.HeaderHomeLink, a#home', el => { el.href = '/'; }); updateRandomLink(); } function updateRandomLink() { const prefGender = getLongGenderName(getCurrentPreferredGender()); let url = '/random/'; if(prefGender) { url += prefGender + '/'; } queryAndRun('#cb-enh-random-cam', el => { el.href = url; }); } function clampInt(value, min, max) { return Math.min(max, Math.max(min, value | 0)); } function stringHasAtLeast(str, ch, maxCount) { let count = 0; for(let i = 0; i < str.length; i++) { if(str[i] === ch) { count++; if(count >= maxCount) return true; } } return false; } function isInteractiveFullScreenEnabled() { if(!isTheaterModeEnabled()) { return false; } const player = document.querySelector('.videoPlayerDiv'); return player.getBoundingClientRect().width === window.innerWidth && player.getBoundingClientRect().height === window.innerHeight; } function enterInteractiveFullScreen() { document.getElementById('fvm-link')?.click(); } function isTheaterModeEnabled() { return document.getElementById('vjs_video_3')?.classList.contains('vjs-controls-disabled'); } function getFileName(fname, ext, date) { const d = date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + ('0' + date.getDate()).slice(-2); const t = ('0' + date.getHours()).slice(-2) + '-' + ('0' + date.getMinutes()).slice(-2) + '-' + ('0' + date.getSeconds()).slice(-2); return fname + '_' + d + '_' + t + ext; } function getFileExtension(fname) { const idx = fname.lastIndexOf('.'); if(idx === -1) { return ''; } return fname.slice(idx + 1).toLowerCase(); } function getCBTrans(url) { const spos1 = url.search('_trns_'); if(spos1 === -1) { return null; } const spos2 = url.substring(0, spos1).lastIndexOf('-'); if(spos2 === -1) { return null; } return url.substring(spos2 + 1, spos1); } function cutPlaylistUrl(hls_source) { let cutFrom = hls_source.indexOf('/playlist.m3u8'); let cutCount = 0; if(cutFrom !== -1) { cutCount = '/playlist.m3u8'.length; } else { cutFrom = hls_source.indexOf('/playlist_sfm4s.m3u8'); if(cutFrom !== -1) { cutCount = '/playlist_sfm4s.m3u8'.length; } } if(cutFrom !== -1) { hls_source = hls_source.slice(0, cutFrom + cutCount); } return hls_source; } function cutLevelUrl(url) { const pos = url.indexOf('chunklist_'); if(pos === -1) { return null; } return url.slice(pos + 11, -5); } function clearAllTimeouts() { let lastID = setTimeout(function() {}, 0); while(lastID--) { clearTimeout(lastID); } } function clearAllIntervals() { let lastID = setInterval(function() {}, 0); while(lastID--) { clearInterval(lastID); } } function getCurrentBroadcasterName() { const el = document.querySelector('span[data-testid="roomSubject-roomName"]'); if(!el) { return null; } return el.innerText; } function getCurrentBroadcasterNameFromURL() { const url = window.location.pathname; if(url.length < 3) { return null; } if(url.includes('-')) { return null; } if(!url.endsWith('/')) { return null; } if(stringHasAtLeast(url, '/', 3)) { return null; } const broadcasterName = url.slice(1, -1); if(['tags', 'discover', 'multicam'].includes(broadcasterName)) { return null; } return broadcasterName; } function isCurrentBroadcasterNotAvailable() { const elOverlay = document.querySelector('div[data-testid="video-overlay-text"]'); if(!elOverlay) { return; } return elOverlay.style.display !== 'none'; } function isRoomStatusNotifierShown() { const el = document.querySelector('div[data-testid="video-overlay-text"]'); if(!el) { return false; } return el.style.display !== 'none'; } function isCurrentRoomOffline() { const el = document.querySelector('.roomStatusNotifier__header'); if(!el) { return null; } return el.innerText === 'Offline'; } function volumeWheelHandler(e) { e.preventDefault(); e.stopImmediatePropagation(); if(!hasUserInteracted()) { return; } let elVideo; let isRewinded = false; const broadcasterName = getCurrentBroadcasterName(); if(broadcasterName && gRewindObjs[broadcasterName] && gRewindObjs[broadcasterName].rewind.isRewinded()) { isRewinded = true; elVideo = getVideoHolderRewindVideo(gRewindObjs[broadcasterName].elVideoHolder); } else { elVideo = getCurrentVideo(); } if(!elVideo) { return; } let vol = elVideo.muted ? 0 : elVideo.volume; if(e.deltaY > 0) { vol -= 0.1; } else if(e.deltaY < 0) { vol += 0.1; } vol = Math.round(vol * 10) / 10; vol = Math.min(vol, 1); vol = Math.max(vol, 0); elVideo.volume = vol; if(elVideo.muted && vol > 0) { elVideo.muted = false; if(!isRewinded) { const elMuteBtn = document.getElementById('volume-mute'); if(elMuteBtn) { elMuteBtn.click(); elVideo.volume = vol; } } } if(isRewinded) { if(vol === 0) { elVideo.muted = true; } updateVideoUI(gRewindObjs[broadcasterName].elVideoHolder); } } const acre7 = 'tVBC55kUB2OqG1orxk2A'; function rev(str) { let rv = ''; for(let i = str.length - 1; i >= 0; i--) { rv += str[i]; } return rv; } const acre8 = '=cCI642ZpFGctF2Y'; function getCSRFToken() { return document.querySelector('input[name="csrfmiddlewaretoken"]')?.value; } let gHideVideoLogoStyle = null; if(getSetting('hide-vid-logo')) { gHideVideoLogoStyle = addStyle('#VideoPanel .cbLogo { display: none; }'); } if(isMultiViewPage() && getSetting('multi-minifscreen')) { addStyle(`#header, #footer-holder, #desktop-spa-header {display: none;}`); } if(getSetting('reg-redir') === 1) { regRedir(getSetting('reg-redir-room')); } document.addEventListener('click', function(e) { if(gRoomMoreMenuVisible && e.target && !e.target.parentElement?.classList.contains('cb-enh-room-more-icon')) { setRoomMoreMenuVisible(false); } if(regRedirPreventClick && e.target !== document.querySelector('.logo-zone a')) { e.stopImmediatePropagation(); e.preventDefault(); return; } if(e.target.nodeName === 'VIDEO') { e.stopImmediatePropagation(); e.preventDefault(); if(document.pictureInPictureElement && (e.target === document.pictureInPictureElement || isMultiViewPage())) { exitVideoPiP(); return; } document.body.click(); return; } if(!gOpenRoomsInNewTab) { return; } const elLink = e.target.closest('a'); if(!elLink && e.target.closest('.RoomCardDetails') && !e.target.closest('.cb-enh-details-icon')) { e.stopImmediatePropagation(); e.target.closest('.RoomCardDetails').parentElement.querySelector('a[data-room]')?.click(); return; } if(!elLink) { return; } if(elLink.target === '_blank') { return; } if(!elLink.hasAttribute('data-room') && !elLink.classList.contains('FollowedDropdown__room-link') && elLink.getAttribute('href') !== '/multicam/') { return; } e.stopImmediatePropagation(); elLink.target = '_blank'; runNextTick(() => { elLink.target = ''; }); }, true); function applySettings() { setChatNoticesHidden(getSetting('chat-hide-notices')); setChatSubjectNoticesHidden(getSetting('chat-hide-subject-notices')); setChatTipsHidden(getSetting('chat-hide-tips')); setChatGreysHidden(getSetting('chat-hide-greys')); setAutoChatRules(getSetting('auto-chat-rules')); setNoSocials(getSetting('no-socials')); setNoMerchLinks(getSetting('no-merch-links')); setNoSurveys(getSetting('no-surveys')); setSmallPageMargins(getSetting('smargins')); setVideoLogoVisible(!getSetting('hide-vid-logo')); gRecordingExitWarn = getSetting('rec-exit-warn'); gRecordAudio = !getSetting('rec-no-audio'); gRecordVideo = !getSetting('rec-no-video'); rewindSetEnabled(getSetting('rewind') !== false); rewindUpdateAll(); gBigPreviewPlayAudio = !getSetting('mute-big-previews'); gBigPreviewEnabled = !getSetting('disable-big-previews'); gMultiUnmuteOnHover = getSetting('multi-unmute-hover'); gOpenRoomsInNewTab = getSetting('open-new-tab'); if(gIsInMultiView) { multiSetAlignRight(getSetting('multi-align-right')); } setStreamDurationEnabled(getSetting('duration') !== false); updateFakeVisibility(); loadGridSize(); loadMoreGridSize(); if(getSetting('blur-mute')) { const elVideo = getCurrentVideo(); if(elVideo && !elVideo.muted && unsafeWindow.__cnh__1 && unsafeWindow.__cnh__1.getVisibilityState() === 'hidden') { blurMuteVideo(elVideo); } } } function settingsApplyRecommended() { for(const [k, v] of Object.entries(SETTINGS_RECOMMENDED)) { setSetting(k, v); } applySettings(); updateSettingsWindow(); } if(!gIsFullVideoPage) { window.addEventListener('storage', (e) => { if(e.key === 'cb-enh-settings') { loadSettings(); if(isMultiViewPage() && !gMultiIsDragging && !gMultiIsResizing) { gMultiCustomGridActive = parseInt(getSetting('multi-grid-size')) === gMultiCustomGridSize && multiLoadRoomsSettings().layoutMode !== 'custom'; } applySettings(); return; } if(e.key === 'cb-enh-blocklist') { blocklistLoad(); return; } }); } function multiToggleCamFullscreen(elBlock) { if(document.fullscreenElement && document.fullscreenElement.classList.contains('cb-enh-multi-cam-wrap2')) { document.exitFullscreen(); } else { elBlock.closest('.cb-enh-multi-cam-wrap2').requestFullscreen(); } } document.addEventListener('dblclick', function(e) { if(isMultiViewPage() && e.target.nodeName === 'VIDEO') { const elVideo = getCurrentVideo(); if(!elVideo || e.target !== elVideo) { multiToggleCamFullscreen(e.target); e.stopImmediatePropagation(); e.preventDefault(); return; } } }); window.addEventListener('beforeunload', function(e) { if(gRecording && gRecordingExitWarn) { e.preventDefault(); e.returnValue = ''; } }); function loadGridSize() { if(gIsFullVideoPage) { return; } const defaultGridSize = 180; const minGridSize = defaultGridSize; const maxGridSize = 300; let size = parseInt(getSetting('grid-size'), 10); if(!Number.isInteger(size)) { size = defaultGridSize; } setGridSize(clampInt(size, minGridSize, maxGridSize)); } loadGridSize(); function loadMoreGridSize() { if(gIsFullVideoPage) { return; } const minGridSize = 180; const maxGridSize = 300; const defaultSize = minGridSize; let size = parseInt(getSetting('grid-more-size'), 10); if(!Number.isInteger(size)) { size = defaultSize; } setMoreRoomsGridSize(clampInt(size, minGridSize, maxGridSize)); } loadMoreGridSize(); function isRoomOnline(username, callback, callbackErr = null) { const onError = callbackErr || (() => callback(false)); return xmlhttpRequest({ method: 'GET', url: 'https://jpeg.live.mmcdn.com/stream?room=' + username + '&f=' + Math.random(), timeout: 10000, responseType: 'arraybuffer', onload: function(req) { if(req.status === 404) { let responseText; try { responseText = new TextDecoder('utf-8').decode(req.response); } catch(e) { callback(false); return; } if(responseText && responseText.trim() === 'img error') { callback(true); return; } callback(false); return; } if(![200, 204].includes(req.status)) { callback(false); return; } callback(req.response.byteLength !== 0); }, onerror: onError, ontimeout: onError, onabort: onError }); } function isRoomOnline_2(username, callback, callbackErr = null) { const onError = callbackErr || (() => callback(false)); const url = 'https://thumb.live.mmcdn.com/riw/' + username + '.jpg'; xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, responseType: 'arraybuffer', onload: function(req) { if(isOfflineThumbnail(req.response.byteLength)) { callback(false); return; } callback(true); }, onerror: onError, ontimeout: onError, onabort: onError }); } function fetchRoomSnapshot(username, callback) { const url = 'https://thumb.live.mmcdn.com/riw/' + username + '.jpg'; xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, responseType: 'arraybuffer', onload: function(req) { if(isOfflineThumbnail(req.response.byteLength)) { return; } callback(req.response, url); } }); } function setFollow(username, doFollow, callbackOk, callbackErr) { const csrfToken = getCSRFToken(); if(!csrfToken) { return; } let url = '/follow/'; if(doFollow) { url += 'follow/'; } else { url += 'unfollow/'; } url += username + '/'; const req = new XMLHttpRequest(); req.timeout = 10000; req.addEventListener('load', function() { let data; try { data = JSON.parse(this.responseText); } catch(e) { callbackErr(); return; } if(!('following' in data)) { callbackErr(); return; } callbackOk(data['following']); }); req.addEventListener('error', function() { callbackErr(); }); req.open('POST', url); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); req.send('location=FollowStar&csrfmiddlewaretoken=' + csrfToken); } if(!document.documentElement) { const documentElementObserver = new MutationObserver(() => { if(document.documentElement) { documentElementObserver.disconnect(); gFunctionsToRun.forEach(fnc => { runFunction(fnc[0], fnc[1]); }); gFunctionsToRun = []; gFunctionsToAdd.forEach(fnc => { addFunction(fnc); }); gFunctionsToAdd = []; gStylesToApply.forEach(style => { addStyle(style); }); gStylesToApply = []; maybeLoadLocales(); } }); documentElementObserver.observe(document, {childList: true}); } else { maybeLoadLocales(); } function updateCurrentVideoTransformStyle(elVideoHolder, data) { elVideoHolder._cnh_videoTransformStyle ??= { mirrored: false, inverted: false, rotation: null } for(const k in data) { elVideoHolder._cnh_videoTransformStyle[k] = data[k]; } const elVideo = getVideoHolderVideoOrRewindVideo(elVideoHolder); if(!elVideo) { return; } elVideo.classList.toggle('cb-enh-video-transformed', elVideoHolder._cnh_videoTransformStyle.mirrored || elVideoHolder._cnh_videoTransformStyle.inverted || elVideoHolder._cnh_videoTransformStyle.rotation !== null); elVideo.closest('.videoPlayerDiv').classList.toggle('cb-enh-video-rotated', elVideoHolder._cnh_videoTransformStyle.rotation !== null); applyCombinedTransform(elVideoHolder); } function applyCombinedTransform(elVideoHolder) { elVideoHolder._cnh_videoTransformStyle ??= { mirrored: false, inverted: false, rotation: null } const elVideo = getVideoHolderVideoOrRewindVideo(elVideoHolder); if(!elVideo) { return; } const videoTransform = buildVideoTransformStyle(elVideo, elVideoHolder._cnh_videoTransformStyle.mirrored, elVideoHolder._cnh_videoTransformStyle.inverted, elVideoHolder._cnh_videoTransformStyle.rotation); const freezoom = gFreezoomInstances.get(elVideo); const state = freezoom ? freezoom.getTransform() : { x: 0, y: 0, zoom: 1 }; let freezoomTransform = ''; if(state.x !== 0 || state.y !== 0 || state.zoom !== 1) { freezoomTransform = `translate(${state.x}px, ${state.y}px) scale(${state.zoom})`; } const finalTransform = `${freezoomTransform || ''} ${videoTransform || ''}`.trim(); elVideo.style.transform = finalTransform; } function buildVideoTransformStyle(el, mirrored, inverted, rotation) { let sx = mirrored ? -1 : 1; let sy = inverted ? -1 : 1; if(rotation !== null) { const ratio = el.offsetHeight / el.offsetWidth; sx *= ratio; sy *= ratio; } let styleScale = ''; if(rotation !== null || mirrored || inverted) { styleScale = `scale(${sx}, ${sy})`; } let styleRot = ''; if(rotation === true) { styleRot = 'rotate(90deg)'; } else if(rotation === false) { styleRot = 'rotate(-90deg)'; } let transformStyle = [styleScale, styleRot].join(' '); if(transformStyle === ' ') { transformStyle = ''; } return transformStyle; } const gFreezoomInstances = new WeakMap(); const gFreezoomElements = new Set(); let gActiveFreezoomInstance = null; function resetFreezoom(elVideoHolder) { const elVideo = getVideoHolderVideoOrRewindVideo(elVideoHolder); if(!elVideo) { return; } const instance = gFreezoomInstances.get(elVideo); if(instance) { instance.resetElement(); } } function freezoomCopy(freezomInstanceCopyTo, freezomInstanceCopyFrom) { if(!freezomInstanceCopyTo || !freezomInstanceCopyFrom) { return; } const fromTransform = freezomInstanceCopyFrom.getTransform(); const toTransform = freezomInstanceCopyTo.getTransform(); toTransform.zoom = fromTransform.zoom; toTransform.x = fromTransform.x; toTransform.y = fromTransform.y; } function getAllFreezoomInstances() { const instances = []; for(const el of gFreezoomElements) { const instance = gFreezoomInstances.get(el); if(instance) { instances.push(instance); } } return instances; } function disableAllFreezoom() { for(const el of [...gFreezoomElements]) { setFreezoomEnabled(el, false, el); } } function getCenterRect(el) { const r = el.getBoundingClientRect(); return { left: r.left, top: r.top, width: r.width, height: r.height, cx: r.width / 2, cy: r.height / 2 }; } function setFreezoomEnabled(elVideoHolder, enabled, elVideoElement = null) { if(!elVideoHolder) { return; } let leftMouseDown = false; let ctrlDown = false; let element; if(elVideoElement === null) { element = getVideoHolderVideo(elVideoHolder); } else { element = elVideoElement; } if(!element) { return; } if(enabled) { if(gFreezoomInstances.has(element)) { return; } const state = { x: 0, y: 0, zoom: 1 }; function resetElement() { state.x = 0; state.y = 0; state.zoom = 1; updateTransform(); updateCursor(); } let freezoomInstance = { getTransform: () => state, resetElement: resetElement, eventHandlers: {} }; gFreezoomInstances.set(element, freezoomInstance); gFreezoomElements.add(element); function constrainPosition() { const parent = element.parentElement; const pw = parent.clientWidth; const ph = parent.clientHeight; const cw = pw * state.zoom; const ch = ph * state.zoom; const minX = (pw - cw) / 2; const maxX = (cw - pw) / 2; const minY = (ph - ch) / 2; const maxY = (ch - ph) / 2; state.x = Math.max(minX, Math.min(maxX, state.x)); state.y = Math.max(minY, Math.min(maxY, state.y)); } function updateCursor() { if((ctrlDown || leftMouseDown) && state.zoom > 1) { element.style.cursor = 'grabbing'; return; } if(state.zoom > 1) { element.style.cursor = 'grab'; return; } if(ctrlDown || leftMouseDown) { element.style.cursor = 'grab'; return; } element.style.cursor = ''; } function updateTransform() { constrainPosition(); applyCombinedTransform(elVideoHolder); } function onKeyDown(e) { ctrlDown = e.ctrlKey || e.metaKey; updateCursor(); } function onKeyUp(e) { ctrlDown = e.ctrlKey || e.metaKey; updateCursor(); } function onWheel(e) { if(!e.ctrlKey && !e.metaKey && !leftMouseDown) { return; } if(gMultiIsDragging) { return; } e.preventDefault(); const oldScale = state.zoom; const newScale = Math.max(1, Math.min(oldScale * (e.deltaY < 0 ? 1.1 : 0.9), 20)); const rect = getCenterRect(element.parentElement); const mx = e.clientX - rect.left - rect.cx; const my = e.clientY - rect.top - rect.cy; const worldX = (mx - state.x) / oldScale; const worldY = (my - state.y) / oldScale; state.zoom = newScale; if(newScale === 1) { state.x = 0; state.y = 0; updateCursor(); updateTransform(); return; } state.x = mx - worldX * newScale; state.y = my - worldY * newScale; updateCursor(); updateTransform(); } function onMouseDown(e) { if(e.button !== 0) { return; } if(gMultiIsDragging) { return; } if(gVideoControlsModalShown) { setVideoControlsVisible(false); } if(element === document.pictureInPictureElement) { exitVideoPiP(); } if(gActiveFreezoomInstance && gActiveFreezoomInstance !== freezoomInstance) { gActiveFreezoomInstance.eventHandlers.onMouseUp(); } gActiveFreezoomInstance = freezoomInstance; leftMouseDown = true; updateCursor(); if(state.zoom <= 1) { return; } } function onDoubleClick(e) { if(state.zoom !== 1) { e.preventDefault(); e.stopImmediatePropagation(); resetElement(); } } function onMouseMove(e) { onMouseMove_2(e.movementX, e.movementY); } function onMouseMove_2(movementX, movementY) { if(!leftMouseDown) { return; } if(state.zoom <= 1) { return; } state.x += movementX; state.y += movementY; updateTransform(); } function onMouseUp() { leftMouseDown = false; if(gActiveFreezoomInstance === freezoomInstance) { gActiveFreezoomInstance = null; } updateCursor(); } element.addEventListener('wheel', onWheel, { passive: false }); element.addEventListener('mousedown', onMouseDown); element.addEventListener('dblclick', onDoubleClick); window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); freezoomInstance.eventHandlers = { onWheel, onMouseDown, onDoubleClick, onMouseMove, onMouseUp, onKeyDown, onKeyUp }; freezoomInstance.onMouseMove_2 = onMouseMove_2; updateTransform(); } else { const instance = gFreezoomInstances.get(element); if(!instance) { return; } if(gActiveFreezoomInstance === instance) { gActiveFreezoomInstance = null; } element.removeEventListener('wheel', instance.eventHandlers.onWheel); element.removeEventListener('mousedown', instance.eventHandlers.onMouseDown); element.removeEventListener('dblclick', instance.eventHandlers.onDoubleClick); window.removeEventListener('mousemove', instance.eventHandlers.onMouseMove); window.removeEventListener('mouseup', instance.eventHandlers.onMouseUp); window.removeEventListener('keydown', instance.eventHandlers.onKeyDown); window.removeEventListener('keyup', instance.eventHandlers.onKeyUp); gFreezoomElements.delete(element); gFreezoomInstances.delete(element); if(element.parentElement) { instance.resetElement(); } element.style.cursor = ''; } } })();